2689 lines
89 KiB
Go
2689 lines
89 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"
|
|
"sort"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/client-go/tools/record"
|
|
"k8s.io/utils/ptr"
|
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
|
|
|
configv1alpha1 "github.com/ai-dynamo/dynamo/deploy/operator/api/config/v1alpha1"
|
|
nvidiacomv1alpha1 "github.com/ai-dynamo/dynamo/deploy/operator/api/v1alpha1"
|
|
"github.com/ai-dynamo/dynamo/deploy/operator/internal/consts"
|
|
commonController "github.com/ai-dynamo/dynamo/deploy/operator/internal/controller_common"
|
|
"github.com/ai-dynamo/dynamo/deploy/operator/internal/dynamo"
|
|
)
|
|
|
|
const (
|
|
testOldWorkerHash = "oldhash1"
|
|
testNewWorkerHash = "newhash2"
|
|
)
|
|
|
|
// createTestDGD creates a DynamoGraphDeployment for testing with the given services
|
|
func createTestDGD(name string, services map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec) *nvidiacomv1alpha1.DynamoGraphDeployment {
|
|
return &nvidiacomv1alpha1.DynamoGraphDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: name,
|
|
Namespace: "default",
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoGraphDeploymentSpec{
|
|
Services: services,
|
|
},
|
|
}
|
|
}
|
|
|
|
// createTestReconciler creates a DynamoGraphDeploymentReconciler for testing
|
|
func createTestReconciler(objs ...runtime.Object) *DynamoGraphDeploymentReconciler {
|
|
scheme := runtime.NewScheme()
|
|
_ = nvidiacomv1alpha1.AddToScheme(scheme)
|
|
_ = corev1.AddToScheme(scheme)
|
|
|
|
fakeClient := fake.NewClientBuilder().
|
|
WithScheme(scheme).
|
|
WithRuntimeObjects(objs...).
|
|
Build()
|
|
|
|
return &DynamoGraphDeploymentReconciler{
|
|
Client: fakeClient,
|
|
Recorder: record.NewFakeRecorder(10),
|
|
Config: &configv1alpha1.OperatorConfiguration{},
|
|
RuntimeConfig: &commonController.RuntimeConfig{},
|
|
DockerSecretRetriever: &mockDockerSecretRetriever{
|
|
GetSecretsFunc: func(namespace, imageName string) ([]string, error) {
|
|
return []string{}, nil
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestShouldTriggerRollingUpdate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
services map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec
|
|
existingHash string // empty means no annotation, "compute" means compute from services
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "new deployment - no hash annotation",
|
|
services: map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
Envs: []corev1.EnvVar{{Name: "FOO", Value: "bar"}},
|
|
},
|
|
},
|
|
existingHash: "",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "hash unchanged - matches current spec",
|
|
services: map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
Envs: []corev1.EnvVar{{Name: "FOO", Value: "bar"}},
|
|
},
|
|
},
|
|
existingHash: "compute",
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "hash changed - differs from current spec",
|
|
services: map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
Envs: []corev1.EnvVar{{Name: "FOO", Value: "new-value"}},
|
|
},
|
|
},
|
|
existingHash: "old-hash-12345678",
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "frontend-only change - hash unchanged",
|
|
services: map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"frontend": {
|
|
ComponentType: consts.ComponentTypeFrontend,
|
|
Envs: []corev1.EnvVar{{Name: "FRONTEND_VAR", Value: "changed"}},
|
|
},
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
Envs: []corev1.EnvVar{{Name: "WORKER_VAR", Value: "unchanged"}},
|
|
},
|
|
},
|
|
existingHash: "compute",
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", tt.services)
|
|
|
|
if tt.existingHash == "compute" {
|
|
hash := dynamo.ComputeDGDWorkersSpecHash(dgd)
|
|
dgd.Annotations = map[string]string{consts.AnnotationCurrentWorkerHash: hash}
|
|
} else if tt.existingHash != "" {
|
|
dgd.Annotations = map[string]string{consts.AnnotationCurrentWorkerHash: tt.existingHash}
|
|
}
|
|
|
|
r := createTestReconciler(dgd)
|
|
result := r.shouldTriggerRollingUpdate(dgd)
|
|
|
|
if result != tt.expected {
|
|
t.Errorf("shouldTriggerRollingUpdate() = %v, expected %v", result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestInitializeWorkerHashIfNeeded_FirstDeploy(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
Envs: []corev1.EnvVar{
|
|
{Name: "FOO", Value: "bar"},
|
|
},
|
|
},
|
|
})
|
|
|
|
// Create reconciler with DGD already in the fake client (simulates existing resource)
|
|
r := createTestReconciler(dgd)
|
|
ctx := context.Background()
|
|
|
|
// Initialize the hash
|
|
err := r.initializeWorkerHashIfNeeded(ctx, dgd)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the hash was set
|
|
hash := r.getCurrentWorkerHash(dgd)
|
|
assert.NotEmpty(t, hash, "Hash should be set after initialization")
|
|
|
|
// Verify the hash is correct
|
|
expectedHash := dynamo.ComputeDGDWorkersSpecHash(dgd)
|
|
assert.Equal(t, expectedHash, hash, "Hash should match computed value")
|
|
}
|
|
|
|
func TestInitializeWorkerHashIfNeeded_AlreadyInitialized(t *testing.T) {
|
|
existingHash := "existing-hash"
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
Envs: []corev1.EnvVar{
|
|
{Name: "FOO", Value: "bar"},
|
|
},
|
|
},
|
|
})
|
|
dgd.Annotations = map[string]string{
|
|
consts.AnnotationCurrentWorkerHash: existingHash,
|
|
}
|
|
|
|
// Create reconciler with DGD already in the fake client
|
|
r := createTestReconciler(dgd)
|
|
ctx := context.Background()
|
|
|
|
// Initialize should be a no-op
|
|
err := r.initializeWorkerHashIfNeeded(ctx, dgd)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the hash was NOT changed
|
|
hash := r.getCurrentWorkerHash(dgd)
|
|
assert.Equal(t, existingHash, hash, "Hash should not change when already initialized")
|
|
}
|
|
|
|
func TestSupportsManagedRollingUpdate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
services map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "standard single-node deployment",
|
|
services: map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "multinode deployment",
|
|
services: map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
Multinode: &nvidiacomv1alpha1.MultinodeSpec{NodeCount: 4},
|
|
},
|
|
},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", tt.services)
|
|
r := createTestReconciler(dgd)
|
|
|
|
result := r.supportsManagedRollingUpdate(dgd)
|
|
if result != tt.expected {
|
|
t.Errorf("isUnsupportedRollingUpdatePathway() = %v, expected %v", result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWorkerHashChanges_OnlyWhenWorkerSpecChanges(t *testing.T) {
|
|
// Test that hash only changes when worker specs change, not frontend specs
|
|
workerEnvs := []corev1.EnvVar{{Name: "WORKER_VAR", Value: "value1"}}
|
|
frontendEnvs := []corev1.EnvVar{{Name: "FRONTEND_VAR", Value: "value1"}}
|
|
|
|
dgd1 := createTestDGD("test", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker, Envs: workerEnvs},
|
|
"frontend": {ComponentType: consts.ComponentTypeFrontend, Envs: frontendEnvs},
|
|
})
|
|
|
|
hash1 := dynamo.ComputeDGDWorkersSpecHash(dgd1)
|
|
|
|
// Change only frontend envs
|
|
dgd2 := createTestDGD("test", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker, Envs: workerEnvs},
|
|
"frontend": {ComponentType: consts.ComponentTypeFrontend, Envs: []corev1.EnvVar{{Name: "FRONTEND_VAR", Value: "changed"}}},
|
|
})
|
|
|
|
hash2 := dynamo.ComputeDGDWorkersSpecHash(dgd2)
|
|
assert.Equal(t, hash1, hash2, "Hash should not change when only frontend changes")
|
|
|
|
// Change worker envs
|
|
dgd3 := createTestDGD("test", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker, Envs: []corev1.EnvVar{{Name: "WORKER_VAR", Value: "changed"}}},
|
|
"frontend": {ComponentType: consts.ComponentTypeFrontend, Envs: frontendEnvs},
|
|
})
|
|
|
|
hash3 := dynamo.ComputeDGDWorkersSpecHash(dgd3)
|
|
assert.NotEqual(t, hash1, hash3, "Hash should change when worker specs change")
|
|
}
|
|
|
|
func TestWorkerHashChanges_PrefillAndDecode(t *testing.T) {
|
|
// Test that prefill and decode component types are also considered workers
|
|
dgd1 := createTestDGD("test", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"prefill": {ComponentType: consts.ComponentTypePrefill, Envs: []corev1.EnvVar{{Name: "VAR", Value: "v1"}}},
|
|
"decode": {ComponentType: consts.ComponentTypeDecode, Envs: []corev1.EnvVar{{Name: "VAR", Value: "v1"}}},
|
|
})
|
|
|
|
hash1 := dynamo.ComputeDGDWorkersSpecHash(dgd1)
|
|
assert.NotEmpty(t, hash1, "Hash should be computed for prefill/decode")
|
|
|
|
// Change prefill spec
|
|
dgd2 := createTestDGD("test", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"prefill": {ComponentType: consts.ComponentTypePrefill, Envs: []corev1.EnvVar{{Name: "VAR", Value: "v2"}}},
|
|
"decode": {ComponentType: consts.ComponentTypeDecode, Envs: []corev1.EnvVar{{Name: "VAR", Value: "v1"}}},
|
|
})
|
|
|
|
hash2 := dynamo.ComputeDGDWorkersSpecHash(dgd2)
|
|
assert.NotEqual(t, hash1, hash2, "Hash should change when prefill specs change")
|
|
|
|
// Change decode spec
|
|
dgd3 := createTestDGD("test", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"prefill": {ComponentType: consts.ComponentTypePrefill, Envs: []corev1.EnvVar{{Name: "VAR", Value: "v1"}}},
|
|
"decode": {ComponentType: consts.ComponentTypeDecode, Envs: []corev1.EnvVar{{Name: "VAR", Value: "v2"}}},
|
|
})
|
|
|
|
hash3 := dynamo.ComputeDGDWorkersSpecHash(dgd3)
|
|
assert.NotEqual(t, hash1, hash3, "Hash should change when decode specs change")
|
|
}
|
|
|
|
func TestGetOrCreateRollingUpdateStatus(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
existingStatus *nvidiacomv1alpha1.RollingUpdateStatus
|
|
expectedPhase nvidiacomv1alpha1.RollingUpdatePhase
|
|
}{
|
|
{
|
|
name: "creates new status when nil",
|
|
existingStatus: nil,
|
|
expectedPhase: nvidiacomv1alpha1.RollingUpdatePhaseNone,
|
|
},
|
|
{
|
|
name: "returns existing status",
|
|
existingStatus: &nvidiacomv1alpha1.RollingUpdateStatus{
|
|
Phase: nvidiacomv1alpha1.RollingUpdatePhaseInProgress,
|
|
},
|
|
expectedPhase: nvidiacomv1alpha1.RollingUpdatePhaseInProgress,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
dgd.Status.RollingUpdate = tt.existingStatus
|
|
|
|
r := createTestReconciler(dgd)
|
|
status := r.getOrCreateRollingUpdateStatus(dgd)
|
|
|
|
assert.NotNil(t, status)
|
|
assert.Equal(t, tt.expectedPhase, status.Phase)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIsRollingUpdateInProgress(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
status *nvidiacomv1alpha1.RollingUpdateStatus
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "nil status - not in progress",
|
|
status: nil,
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "phase none - not in progress",
|
|
status: &nvidiacomv1alpha1.RollingUpdateStatus{Phase: nvidiacomv1alpha1.RollingUpdatePhaseNone},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "phase pending - in progress",
|
|
status: &nvidiacomv1alpha1.RollingUpdateStatus{Phase: nvidiacomv1alpha1.RollingUpdatePhasePending},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "phase in progress - in progress",
|
|
status: &nvidiacomv1alpha1.RollingUpdateStatus{Phase: nvidiacomv1alpha1.RollingUpdatePhaseInProgress},
|
|
expected: true,
|
|
},
|
|
{
|
|
name: "phase completed - not in progress",
|
|
status: &nvidiacomv1alpha1.RollingUpdateStatus{Phase: nvidiacomv1alpha1.RollingUpdatePhaseCompleted},
|
|
expected: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
dgd.Status.RollingUpdate = tt.status
|
|
|
|
r := createTestReconciler(dgd)
|
|
result := r.isRollingUpdateInProgress(dgd)
|
|
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetDesiredWorkerReplicas(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
services map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec
|
|
expected int32
|
|
}{
|
|
{
|
|
name: "single worker with replicas",
|
|
services: map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
Replicas: ptr.To(int32(3)),
|
|
},
|
|
},
|
|
expected: 3,
|
|
},
|
|
{
|
|
name: "single worker without replicas defaults to 1",
|
|
services: map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
},
|
|
},
|
|
expected: 1,
|
|
},
|
|
{
|
|
name: "multiple workers",
|
|
services: map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"prefill": {
|
|
ComponentType: consts.ComponentTypePrefill,
|
|
Replicas: ptr.To(int32(2)),
|
|
},
|
|
"decode": {
|
|
ComponentType: consts.ComponentTypeDecode,
|
|
Replicas: ptr.To(int32(4)),
|
|
},
|
|
},
|
|
expected: 6,
|
|
},
|
|
{
|
|
name: "workers and frontend - only counts workers",
|
|
services: map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"frontend": {
|
|
ComponentType: consts.ComponentTypeFrontend,
|
|
Replicas: ptr.To(int32(2)),
|
|
},
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
Replicas: ptr.To(int32(3)),
|
|
},
|
|
},
|
|
expected: 3,
|
|
},
|
|
{
|
|
name: "no workers",
|
|
services: map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{},
|
|
expected: 0,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", tt.services)
|
|
r := createTestReconciler(dgd)
|
|
|
|
result := r.getDesiredWorkerReplicas(dgd)
|
|
assert.Equal(t, tt.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeleteOldWorkerDCDs(t *testing.T) {
|
|
newWorkerHash := testNewWorkerHash
|
|
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
|
|
// Create DCD with old worker hash
|
|
oldDCD1 := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-oldhash1",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: testOldWorkerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Create DCD with new worker hash (should not be deleted)
|
|
newDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-newhash2",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: newWorkerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, oldDCD1, newDCD)
|
|
ctx := context.Background()
|
|
|
|
// Delete old worker DCDs
|
|
err := r.deleteOldWorkerDCDs(ctx, dgd, newWorkerHash)
|
|
require.NoError(t, err)
|
|
|
|
// Verify old DCD is deleted
|
|
dcdList := &nvidiacomv1alpha1.DynamoComponentDeploymentList{}
|
|
err = r.List(ctx, dcdList)
|
|
require.NoError(t, err)
|
|
|
|
// Should only have the new DCD remaining
|
|
assert.Len(t, dcdList.Items, 1)
|
|
assert.Equal(t, "test-dgd-worker-newhash2", dcdList.Items[0].Name)
|
|
}
|
|
|
|
func TestDeleteOldWorkerDCDs_NoDCDsToDelete(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
|
|
r := createTestReconciler(dgd)
|
|
ctx := context.Background()
|
|
|
|
// Delete old worker DCDs when there are none - should not error
|
|
err := r.deleteOldWorkerDCDs(ctx, dgd, "somehash")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// createTestReconcilerWithStatus creates a reconciler with status subresource support.
|
|
func createTestReconcilerWithStatus(dgd *nvidiacomv1alpha1.DynamoGraphDeployment, objs ...runtime.Object) *DynamoGraphDeploymentReconciler {
|
|
scheme := runtime.NewScheme()
|
|
_ = nvidiacomv1alpha1.AddToScheme(scheme)
|
|
_ = corev1.AddToScheme(scheme)
|
|
|
|
allObjs := append([]runtime.Object{dgd}, objs...)
|
|
|
|
fakeClient := fake.NewClientBuilder().
|
|
WithScheme(scheme).
|
|
WithRuntimeObjects(allObjs...).
|
|
WithStatusSubresource(&nvidiacomv1alpha1.DynamoGraphDeployment{}).
|
|
Build()
|
|
|
|
return &DynamoGraphDeploymentReconciler{
|
|
Client: fakeClient,
|
|
Recorder: record.NewFakeRecorder(10),
|
|
}
|
|
}
|
|
|
|
func TestContinueRollingUpdate_UpdatedServicesPartialCompletion(t *testing.T) {
|
|
oldWorkerHash := testOldWorkerHash
|
|
newWorkerHash := testNewWorkerHash
|
|
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"prefill": {
|
|
ComponentType: consts.ComponentTypePrefill,
|
|
Replicas: ptr.To(int32(2)),
|
|
},
|
|
"decode": {
|
|
ComponentType: consts.ComponentTypeDecode,
|
|
Replicas: ptr.To(int32(3)),
|
|
},
|
|
})
|
|
dgd.Annotations = map[string]string{
|
|
consts.AnnotationCurrentWorkerHash: oldWorkerHash,
|
|
}
|
|
dgd.Status.RollingUpdate = &nvidiacomv1alpha1.RollingUpdateStatus{
|
|
Phase: nvidiacomv1alpha1.RollingUpdatePhaseInProgress,
|
|
}
|
|
|
|
// New DCDs: prefill fully ready, decode not ready yet
|
|
newPrefillDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-prefill-" + newWorkerHash[:8],
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: newWorkerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypePrefill,
|
|
ServiceName: "prefill",
|
|
Replicas: ptr.To(int32(2)),
|
|
},
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
Service: &nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
ReadyReplicas: ptr.To(int32(2)),
|
|
},
|
|
},
|
|
}
|
|
|
|
newDecodeDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-decode-" + newWorkerHash[:8],
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: newWorkerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeDecode,
|
|
ServiceName: "decode",
|
|
Replicas: ptr.To(int32(3)),
|
|
},
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
Service: &nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
ReadyReplicas: ptr.To(int32(1)), // Not yet fully ready
|
|
},
|
|
},
|
|
}
|
|
|
|
// Old DCDs: prefill gone, decode still has replicas
|
|
oldDecodeDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-decode-" + oldWorkerHash[:8],
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: oldWorkerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeDecode,
|
|
ServiceName: "decode",
|
|
Replicas: ptr.To(int32(3)),
|
|
},
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
Service: &nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
ReadyReplicas: ptr.To(int32(2)), // Still has old replicas
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconcilerWithStatus(dgd, newPrefillDCD, newDecodeDCD, oldDecodeDCD)
|
|
ctx := context.Background()
|
|
|
|
rollingUpdateStatus := dgd.Status.RollingUpdate
|
|
err := r.continueRollingUpdate(ctx, dgd, rollingUpdateStatus, newWorkerHash)
|
|
require.NoError(t, err)
|
|
|
|
// Prefill is updated (new ready >= desired, old gone), decode is not
|
|
assert.Equal(t, []string{"prefill"}, rollingUpdateStatus.UpdatedServices)
|
|
// Rolling update should remain in progress since not all services are updated
|
|
assert.Equal(t, nvidiacomv1alpha1.RollingUpdatePhaseInProgress, rollingUpdateStatus.Phase)
|
|
}
|
|
|
|
func TestContinueRollingUpdate_AggregateReadyButPerServiceNot(t *testing.T) {
|
|
oldWorkerHash := testOldWorkerHash
|
|
newWorkerHash := testNewWorkerHash
|
|
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"prefill": {
|
|
ComponentType: consts.ComponentTypePrefill,
|
|
Replicas: ptr.To(int32(2)),
|
|
},
|
|
"decode": {
|
|
ComponentType: consts.ComponentTypeDecode,
|
|
Replicas: ptr.To(int32(3)),
|
|
},
|
|
})
|
|
dgd.Annotations = map[string]string{
|
|
consts.AnnotationCurrentWorkerHash: oldWorkerHash,
|
|
}
|
|
dgd.Status.RollingUpdate = &nvidiacomv1alpha1.RollingUpdateStatus{
|
|
Phase: nvidiacomv1alpha1.RollingUpdatePhaseInProgress,
|
|
}
|
|
|
|
// New DCDs: prefill has excess ready replicas (5), decode has 0
|
|
// Aggregate: 5 total new ready >= 5 desired, 0 old ready == 0
|
|
// Per-service: prefill ready (5 >= 2), decode NOT ready (0 < 3)
|
|
newPrefillDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-prefill-" + newWorkerHash[:8],
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: newWorkerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypePrefill,
|
|
ServiceName: "prefill",
|
|
Replicas: ptr.To(int32(2)),
|
|
},
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
Service: &nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
ReadyReplicas: ptr.To(int32(5)), // Excess ready replicas
|
|
},
|
|
},
|
|
}
|
|
|
|
newDecodeDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-decode-" + newWorkerHash[:8],
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: newWorkerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeDecode,
|
|
ServiceName: "decode",
|
|
Replicas: ptr.To(int32(3)),
|
|
},
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
Service: &nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
ReadyReplicas: ptr.To(int32(0)), // No ready replicas
|
|
},
|
|
},
|
|
}
|
|
|
|
// No old DCDs — old workers are fully scaled down
|
|
r := createTestReconcilerWithStatus(dgd, newPrefillDCD, newDecodeDCD)
|
|
ctx := context.Background()
|
|
|
|
rollingUpdateStatus := dgd.Status.RollingUpdate
|
|
err := r.continueRollingUpdate(ctx, dgd, rollingUpdateStatus, newWorkerHash)
|
|
require.NoError(t, err)
|
|
|
|
// Only prefill is updated; decode has 0 ready replicas
|
|
assert.Equal(t, []string{"prefill"}, rollingUpdateStatus.UpdatedServices)
|
|
// Rolling update must NOT complete — decode is not ready
|
|
assert.Equal(t, nvidiacomv1alpha1.RollingUpdatePhaseInProgress, rollingUpdateStatus.Phase)
|
|
}
|
|
|
|
func TestStartRollingUpdate_UpdatedServicesInitializedToNil(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
Replicas: ptr.To(int32(2)),
|
|
},
|
|
})
|
|
dgd.Annotations = map[string]string{
|
|
consts.AnnotationCurrentWorkerHash: testOldWorkerHash,
|
|
}
|
|
// Simulate a previous rolling update that had UpdatedServices populated
|
|
dgd.Status.RollingUpdate = &nvidiacomv1alpha1.RollingUpdateStatus{
|
|
Phase: nvidiacomv1alpha1.RollingUpdatePhaseNone,
|
|
UpdatedServices: []string{"worker"},
|
|
}
|
|
|
|
r := createTestReconcilerWithStatus(dgd)
|
|
ctx := context.Background()
|
|
|
|
rollingUpdateStatus := dgd.Status.RollingUpdate
|
|
err := r.startRollingUpdate(ctx, dgd, rollingUpdateStatus, testNewWorkerHash)
|
|
require.NoError(t, err)
|
|
|
|
assert.Nil(t, rollingUpdateStatus.UpdatedServices)
|
|
assert.Equal(t, nvidiacomv1alpha1.RollingUpdatePhasePending, rollingUpdateStatus.Phase)
|
|
}
|
|
|
|
func TestCompleteRollingUpdate_UpdatedServicesContainsAllWorkers(t *testing.T) {
|
|
oldWorkerHash := testOldWorkerHash
|
|
newWorkerHash := testNewWorkerHash
|
|
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"frontend": {
|
|
ComponentType: consts.ComponentTypeFrontend,
|
|
Replicas: ptr.To(int32(1)),
|
|
},
|
|
"prefill": {
|
|
ComponentType: consts.ComponentTypePrefill,
|
|
Replicas: ptr.To(int32(2)),
|
|
},
|
|
"decode": {
|
|
ComponentType: consts.ComponentTypeDecode,
|
|
Replicas: ptr.To(int32(3)),
|
|
},
|
|
})
|
|
dgd.Annotations = map[string]string{
|
|
consts.AnnotationCurrentWorkerHash: oldWorkerHash,
|
|
}
|
|
dgd.Status.RollingUpdate = &nvidiacomv1alpha1.RollingUpdateStatus{
|
|
Phase: nvidiacomv1alpha1.RollingUpdatePhaseInProgress,
|
|
}
|
|
|
|
r := createTestReconcilerWithStatus(dgd)
|
|
ctx := context.Background()
|
|
|
|
rollingUpdateStatus := dgd.Status.RollingUpdate
|
|
err := r.completeRollingUpdate(ctx, dgd, rollingUpdateStatus, newWorkerHash)
|
|
require.NoError(t, err)
|
|
|
|
// Should contain all worker services (sorted), but not frontend
|
|
assert.Equal(t, []string{"decode", "prefill"}, rollingUpdateStatus.UpdatedServices)
|
|
assert.Equal(t, nvidiacomv1alpha1.RollingUpdatePhaseCompleted, rollingUpdateStatus.Phase)
|
|
assert.NotNil(t, rollingUpdateStatus.EndTime)
|
|
}
|
|
|
|
func TestContinueRollingUpdate_AllServicesUpdated(t *testing.T) {
|
|
oldWorkerHash := testOldWorkerHash
|
|
newWorkerHash := testNewWorkerHash
|
|
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"prefill": {
|
|
ComponentType: consts.ComponentTypePrefill,
|
|
Replicas: ptr.To(int32(2)),
|
|
},
|
|
"decode": {
|
|
ComponentType: consts.ComponentTypeDecode,
|
|
Replicas: ptr.To(int32(3)),
|
|
},
|
|
})
|
|
dgd.Annotations = map[string]string{
|
|
consts.AnnotationCurrentWorkerHash: oldWorkerHash,
|
|
}
|
|
dgd.Status.RollingUpdate = &nvidiacomv1alpha1.RollingUpdateStatus{
|
|
Phase: nvidiacomv1alpha1.RollingUpdatePhaseInProgress,
|
|
}
|
|
|
|
// All new DCDs fully ready
|
|
newPrefillDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-prefill-" + newWorkerHash[:8],
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: newWorkerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypePrefill,
|
|
ServiceName: "prefill",
|
|
Replicas: ptr.To(int32(2)),
|
|
},
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
Service: &nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
ReadyReplicas: ptr.To(int32(2)),
|
|
},
|
|
},
|
|
}
|
|
|
|
newDecodeDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-decode-" + newWorkerHash[:8],
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: newWorkerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeDecode,
|
|
ServiceName: "decode",
|
|
Replicas: ptr.To(int32(3)),
|
|
},
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
Service: &nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
ReadyReplicas: ptr.To(int32(3)),
|
|
},
|
|
},
|
|
}
|
|
|
|
// No old DCDs (all scaled down and removed)
|
|
|
|
r := createTestReconcilerWithStatus(dgd, newPrefillDCD, newDecodeDCD)
|
|
ctx := context.Background()
|
|
|
|
rollingUpdateStatus := dgd.Status.RollingUpdate
|
|
err := r.continueRollingUpdate(ctx, dgd, rollingUpdateStatus, newWorkerHash)
|
|
require.NoError(t, err)
|
|
|
|
// Rolling update should complete, and all services should be listed
|
|
assert.Equal(t, nvidiacomv1alpha1.RollingUpdatePhaseCompleted, rollingUpdateStatus.Phase)
|
|
assert.Equal(t, []string{"decode", "prefill"}, rollingUpdateStatus.UpdatedServices)
|
|
}
|
|
|
|
func TestGetWorkerInfoForWorkerHash(t *testing.T) {
|
|
workerHash := "hash1234"
|
|
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"prefill": {ComponentType: consts.ComponentTypePrefill},
|
|
"decode": {ComponentType: consts.ComponentTypeDecode},
|
|
})
|
|
|
|
// Create DCDs for prefill and decode with different ready counts
|
|
prefillDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-prefill-hash1234",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: workerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypePrefill,
|
|
ServiceName: "prefill",
|
|
Replicas: ptr.To(int32(2)),
|
|
},
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
Service: &nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
ReadyReplicas: ptr.To(int32(2)),
|
|
},
|
|
},
|
|
}
|
|
|
|
decodeDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-decode-hash1234",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: workerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeDecode,
|
|
ServiceName: "decode",
|
|
Replicas: ptr.To(int32(3)),
|
|
},
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
Service: &nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
ReadyReplicas: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, prefillDCD, decodeDCD)
|
|
ctx := context.Background()
|
|
|
|
status, err := r.getWorkerInfoForWorkerHash(ctx, dgd, workerHash)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, status.services, 2)
|
|
assert.Equal(t, int32(2), status.services[consts.ComponentTypePrefill].readyReplicas)
|
|
assert.Equal(t, int32(1), status.services[consts.ComponentTypeDecode].readyReplicas)
|
|
assert.Equal(t, int32(3), status.totalReadyWorkers) // 2 + 1
|
|
}
|
|
|
|
func TestMergeWorkerServiceStatuses(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
serviceStatuses map[string]nvidiacomv1alpha1.ServiceReplicaStatus
|
|
oldWorkerStatuses map[string]nvidiacomv1alpha1.ServiceReplicaStatus
|
|
expected map[string]nvidiacomv1alpha1.ServiceReplicaStatus
|
|
}{
|
|
{
|
|
name: "merges old and new for a single worker service",
|
|
serviceStatuses: map[string]nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
"prefill": {
|
|
ComponentKind: "Deployment",
|
|
ComponentName: "dgd-prefill-newhash1",
|
|
Replicas: 2,
|
|
UpdatedReplicas: 2,
|
|
ReadyReplicas: ptr.To(int32(2)),
|
|
AvailableReplicas: ptr.To(int32(2)),
|
|
},
|
|
},
|
|
oldWorkerStatuses: map[string]nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
"prefill": {
|
|
ComponentKind: "Deployment",
|
|
ComponentName: "dgd-prefill-oldhash1",
|
|
ComponentNames: []string{"dgd-prefill-oldhash1"},
|
|
Replicas: 1,
|
|
UpdatedReplicas: 0,
|
|
ReadyReplicas: ptr.To(int32(1)),
|
|
AvailableReplicas: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
expected: map[string]nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
"prefill": {
|
|
ComponentKind: "Deployment",
|
|
ComponentName: "dgd-prefill-newhash1",
|
|
ComponentNames: []string{"dgd-prefill-newhash1", "dgd-prefill-oldhash1"},
|
|
Replicas: 3,
|
|
UpdatedReplicas: 2, // Only new are "updated"
|
|
ReadyReplicas: ptr.To(int32(3)),
|
|
AvailableReplicas: ptr.To(int32(3)),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "no old statuses - no-op",
|
|
serviceStatuses: map[string]nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
"prefill": {
|
|
ComponentKind: "Deployment",
|
|
ComponentName: "dgd-prefill-newhash1",
|
|
Replicas: 2,
|
|
ReadyReplicas: ptr.To(int32(2)),
|
|
},
|
|
},
|
|
oldWorkerStatuses: map[string]nvidiacomv1alpha1.ServiceReplicaStatus{},
|
|
expected: map[string]nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
"prefill": {
|
|
ComponentKind: "Deployment",
|
|
ComponentName: "dgd-prefill-newhash1",
|
|
Replicas: 2,
|
|
ReadyReplicas: ptr.To(int32(2)),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "old exists but new doesn't yet",
|
|
serviceStatuses: map[string]nvidiacomv1alpha1.ServiceReplicaStatus{},
|
|
oldWorkerStatuses: map[string]nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
"prefill": {
|
|
ComponentKind: "Deployment",
|
|
ComponentName: "dgd-prefill-oldhash1",
|
|
Replicas: 2,
|
|
ReadyReplicas: ptr.To(int32(2)),
|
|
},
|
|
},
|
|
expected: map[string]nvidiacomv1alpha1.ServiceReplicaStatus{},
|
|
},
|
|
{
|
|
name: "handles nil ReadyReplicas and AvailableReplicas on old",
|
|
serviceStatuses: map[string]nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
"prefill": {
|
|
ComponentKind: "Deployment",
|
|
ComponentName: "dgd-prefill-newhash1",
|
|
Replicas: 2,
|
|
ReadyReplicas: ptr.To(int32(2)),
|
|
AvailableReplicas: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
oldWorkerStatuses: map[string]nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
"prefill": {
|
|
ComponentKind: "Deployment",
|
|
ComponentName: "dgd-prefill-oldhash1",
|
|
ComponentNames: []string{"dgd-prefill-oldhash1"},
|
|
Replicas: 1,
|
|
ReadyReplicas: nil,
|
|
AvailableReplicas: nil,
|
|
},
|
|
},
|
|
expected: map[string]nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
"prefill": {
|
|
ComponentKind: "Deployment",
|
|
ComponentName: "dgd-prefill-newhash1",
|
|
ComponentNames: []string{"dgd-prefill-newhash1", "dgd-prefill-oldhash1"},
|
|
Replicas: 3,
|
|
ReadyReplicas: ptr.To(int32(2)),
|
|
AvailableReplicas: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "frontend status untouched by merge",
|
|
serviceStatuses: map[string]nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
"frontend": {
|
|
ComponentKind: "Deployment",
|
|
ComponentName: "dgd-frontend",
|
|
Replicas: 1,
|
|
ReadyReplicas: ptr.To(int32(1)),
|
|
},
|
|
"prefill": {
|
|
ComponentKind: "Deployment",
|
|
ComponentName: "dgd-prefill-newhash1",
|
|
Replicas: 2,
|
|
ReadyReplicas: ptr.To(int32(2)),
|
|
},
|
|
},
|
|
oldWorkerStatuses: map[string]nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
"prefill": {
|
|
ComponentKind: "Deployment",
|
|
ComponentName: "dgd-prefill-oldhash1",
|
|
ComponentNames: []string{"dgd-prefill-oldhash1"},
|
|
Replicas: 1,
|
|
ReadyReplicas: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
expected: map[string]nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
"frontend": {
|
|
ComponentKind: "Deployment",
|
|
ComponentName: "dgd-frontend",
|
|
Replicas: 1,
|
|
ReadyReplicas: ptr.To(int32(1)),
|
|
},
|
|
"prefill": {
|
|
ComponentKind: "Deployment",
|
|
ComponentName: "dgd-prefill-newhash1",
|
|
ComponentNames: []string{"dgd-prefill-newhash1", "dgd-prefill-oldhash1"},
|
|
Replicas: 3,
|
|
ReadyReplicas: ptr.To(int32(3)),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mergeWorkerServiceStatuses(tt.serviceStatuses, tt.oldWorkerStatuses)
|
|
assert.Equal(t, tt.expected, tt.serviceStatuses)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAggregateOldWorkerServiceStatuses(t *testing.T) {
|
|
t.Run("old DCD exists with status", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"prefill": {
|
|
ComponentType: consts.ComponentTypePrefill,
|
|
Replicas: ptr.To(int32(2)),
|
|
},
|
|
})
|
|
|
|
oldDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-prefill-oldhash1",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: testOldWorkerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypePrefill,
|
|
ServiceName: "prefill",
|
|
Replicas: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
Service: &nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
ComponentKind: "Deployment",
|
|
ComponentName: "test-dgd-prefill-oldhash1",
|
|
Replicas: 1,
|
|
UpdatedReplicas: 0,
|
|
ReadyReplicas: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, oldDCD)
|
|
ctx := context.Background()
|
|
|
|
rollingUpdateCtx := dynamo.RollingUpdateContext{
|
|
NewWorkerHash: testNewWorkerHash,
|
|
OldWorkerReplicas: map[string]int32{"prefill": 1},
|
|
NewWorkerReplicas: map[string]int32{"prefill": 2},
|
|
}
|
|
|
|
statuses, err := r.aggregateOldWorkerServiceStatuses(ctx, dgd, rollingUpdateCtx)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, statuses, 1)
|
|
assert.Equal(t, "test-dgd-prefill-oldhash1", statuses["prefill"].ComponentName)
|
|
assert.Equal(t, []string{"test-dgd-prefill-oldhash1"}, statuses["prefill"].ComponentNames)
|
|
assert.Equal(t, int32(1), statuses["prefill"].Replicas)
|
|
assert.Equal(t, ptr.To(int32(1)), statuses["prefill"].ReadyReplicas)
|
|
})
|
|
|
|
t.Run("old DCD not found - skips gracefully", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"prefill": {
|
|
ComponentType: consts.ComponentTypePrefill,
|
|
Replicas: ptr.To(int32(2)),
|
|
},
|
|
})
|
|
|
|
r := createTestReconciler(dgd)
|
|
ctx := context.Background()
|
|
|
|
rollingUpdateCtx := dynamo.RollingUpdateContext{
|
|
NewWorkerHash: testNewWorkerHash,
|
|
OldWorkerReplicas: map[string]int32{"prefill": 1},
|
|
NewWorkerReplicas: map[string]int32{"prefill": 2},
|
|
}
|
|
|
|
statuses, err := r.aggregateOldWorkerServiceStatuses(ctx, dgd, rollingUpdateCtx)
|
|
require.NoError(t, err)
|
|
|
|
assert.Empty(t, statuses)
|
|
})
|
|
}
|
|
|
|
func TestGetExistingRestartAnnotationsDCD(t *testing.T) {
|
|
t.Run("worker DCD with hash suffix - finds annotation", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"frontend": {
|
|
ComponentType: consts.ComponentTypeFrontend,
|
|
},
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
},
|
|
})
|
|
// Annotation hash can differ from computed hash — function uses computed hash
|
|
computedHash := dynamo.ComputeDGDWorkersSpecHash(dgd)
|
|
dgd.Annotations = map[string]string{
|
|
consts.AnnotationCurrentWorkerHash: "oldhash",
|
|
}
|
|
|
|
frontendDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-frontend",
|
|
Namespace: "default",
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
Annotations: map[string]string{
|
|
consts.RestartAnnotation: "2025-01-01T00:00:00Z",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
workerDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-" + computedHash,
|
|
Namespace: "default",
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
Annotations: map[string]string{
|
|
consts.RestartAnnotation: "2025-01-01T00:00:00Z",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, frontendDCD, workerDCD)
|
|
ctx := context.Background()
|
|
|
|
annotations, err := r.getExistingRestartAnnotationsDCD(ctx, dgd)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "2025-01-01T00:00:00Z", annotations["frontend"])
|
|
assert.Equal(t, "2025-01-01T00:00:00Z", annotations["worker"])
|
|
})
|
|
|
|
t.Run("worker DCD not found during rolling update - gracefully skips", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"frontend": {
|
|
ComponentType: consts.ComponentTypeFrontend,
|
|
},
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
},
|
|
})
|
|
dgd.Annotations = map[string]string{
|
|
consts.AnnotationCurrentWorkerHash: testOldWorkerHash,
|
|
}
|
|
|
|
frontendDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-frontend",
|
|
Namespace: "default",
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
Annotations: map[string]string{
|
|
consts.RestartAnnotation: "2025-01-01T00:00:00Z",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, frontendDCD)
|
|
ctx := context.Background()
|
|
|
|
annotations, err := r.getExistingRestartAnnotationsDCD(ctx, dgd)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "2025-01-01T00:00:00Z", annotations["frontend"])
|
|
_, hasWorker := annotations["worker"]
|
|
assert.False(t, hasWorker, "worker annotation should not be present when DCD doesn't exist")
|
|
})
|
|
|
|
t.Run("non-worker without hash suffix - found normally", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"frontend": {
|
|
ComponentType: consts.ComponentTypeFrontend,
|
|
},
|
|
})
|
|
dgd.Annotations = map[string]string{
|
|
consts.AnnotationCurrentWorkerHash: testOldWorkerHash,
|
|
}
|
|
|
|
frontendDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-frontend",
|
|
Namespace: "default",
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
Annotations: map[string]string{
|
|
consts.RestartAnnotation: "2025-01-01T00:00:00Z",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, frontendDCD)
|
|
ctx := context.Background()
|
|
|
|
annotations, err := r.getExistingRestartAnnotationsDCD(ctx, dgd)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "2025-01-01T00:00:00Z", annotations["frontend"])
|
|
})
|
|
}
|
|
|
|
func TestCheckComponentServiceFullyUpdated(t *testing.T) {
|
|
t.Run("worker with hash suffix - finds DCD", func(t *testing.T) {
|
|
workerHash := "abc12345"
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
},
|
|
})
|
|
dgd.Annotations = map[string]string{
|
|
consts.AnnotationCurrentWorkerHash: workerHash + "fullhashextra",
|
|
}
|
|
|
|
workerDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-" + workerHash + "fullhashextra",
|
|
Namespace: "default",
|
|
Generation: 1,
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
ObservedGeneration: 1,
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: nvidiacomv1alpha1.DynamoGraphDeploymentConditionTypeAvailable,
|
|
Status: metav1.ConditionTrue,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, workerDCD)
|
|
ctx := context.Background()
|
|
|
|
isReady, reason := r.checkComponentServiceFullyUpdated(ctx, dgd, "worker")
|
|
assert.True(t, isReady, "worker DCD should be ready")
|
|
assert.Empty(t, reason)
|
|
})
|
|
|
|
t.Run("non-worker without hash suffix - finds DCD", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"frontend": {
|
|
ComponentType: consts.ComponentTypeFrontend,
|
|
},
|
|
})
|
|
|
|
frontendDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-frontend",
|
|
Namespace: "default",
|
|
Generation: 1,
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
ObservedGeneration: 1,
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: nvidiacomv1alpha1.DynamoGraphDeploymentConditionTypeAvailable,
|
|
Status: metav1.ConditionTrue,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, frontendDCD)
|
|
ctx := context.Background()
|
|
|
|
isReady, reason := r.checkComponentServiceFullyUpdated(ctx, dgd, "frontend")
|
|
assert.True(t, isReady, "frontend DCD should be ready")
|
|
assert.Empty(t, reason)
|
|
})
|
|
|
|
t.Run("worker without hash annotation - falls back to non-hash name", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
},
|
|
})
|
|
// No worker hash annotation
|
|
|
|
workerDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker",
|
|
Namespace: "default",
|
|
Generation: 1,
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
ObservedGeneration: 1,
|
|
Conditions: []metav1.Condition{
|
|
{
|
|
Type: nvidiacomv1alpha1.DynamoGraphDeploymentConditionTypeAvailable,
|
|
Status: metav1.ConditionTrue,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, workerDCD)
|
|
ctx := context.Background()
|
|
|
|
isReady, reason := r.checkComponentServiceFullyUpdated(ctx, dgd, "worker")
|
|
assert.True(t, isReady, "worker DCD should be ready via fallback")
|
|
assert.Empty(t, reason)
|
|
})
|
|
}
|
|
|
|
func TestInitializeWorkerHashIfNeeded_LegacyDCDsMigration(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
Envs: []corev1.EnvVar{{Name: "FOO", Value: "bar"}},
|
|
},
|
|
})
|
|
|
|
// Create a legacy worker DCD: has DGD name label but NO worker hash label.
|
|
// This simulates a DCD created by a pre-rolling-update operator version.
|
|
legacyWorkerDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
// Note: No KubeLabelDynamoWorkerHash label
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
ServiceName: "worker",
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, legacyWorkerDCD)
|
|
ctx := context.Background()
|
|
|
|
err := r.initializeWorkerHashIfNeeded(ctx, dgd)
|
|
require.NoError(t, err)
|
|
|
|
// DGD annotation should be set to the legacy sentinel, NOT the computed hash
|
|
hash := r.getCurrentWorkerHash(dgd)
|
|
assert.Equal(t, consts.LegacyWorkerHash, hash, "Hash should be legacy sentinel after migration")
|
|
|
|
// Legacy DCD should now have the worker hash label backfilled
|
|
updatedDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{}
|
|
err = r.Get(ctx, types.NamespacedName{Name: "test-dgd-worker", Namespace: "default"}, updatedDCD)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, consts.LegacyWorkerHash, updatedDCD.Labels[consts.KubeLabelDynamoWorkerHash],
|
|
"Legacy DCD should have worker hash label backfilled")
|
|
}
|
|
|
|
func TestInitializeWorkerHashIfNeeded_LegacyMultipleWorkers(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"prefill": {
|
|
ComponentType: consts.ComponentTypePrefill,
|
|
},
|
|
"decode": {
|
|
ComponentType: consts.ComponentTypeDecode,
|
|
},
|
|
"frontend": {
|
|
ComponentType: consts.ComponentTypeFrontend,
|
|
},
|
|
})
|
|
|
|
// Legacy worker DCDs (no hash label)
|
|
legacyPrefillDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-prefill",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypePrefill,
|
|
ServiceName: "prefill",
|
|
},
|
|
},
|
|
}
|
|
|
|
legacyDecodeDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-decode",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeDecode,
|
|
ServiceName: "decode",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Frontend DCD (not a worker, should not be touched)
|
|
frontendDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-frontend",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeFrontend,
|
|
ServiceName: "frontend",
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, legacyPrefillDCD, legacyDecodeDCD, frontendDCD)
|
|
ctx := context.Background()
|
|
|
|
err := r.initializeWorkerHashIfNeeded(ctx, dgd)
|
|
require.NoError(t, err)
|
|
|
|
// DGD should have legacy sentinel hash
|
|
assert.Equal(t, consts.LegacyWorkerHash, r.getCurrentWorkerHash(dgd))
|
|
|
|
// Both worker DCDs should have hash label backfilled
|
|
for _, name := range []string{"test-dgd-prefill", "test-dgd-decode"} {
|
|
dcd := &nvidiacomv1alpha1.DynamoComponentDeployment{}
|
|
err = r.Get(ctx, types.NamespacedName{Name: name, Namespace: "default"}, dcd)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, consts.LegacyWorkerHash, dcd.Labels[consts.KubeLabelDynamoWorkerHash],
|
|
"Worker DCD %s should have legacy hash label", name)
|
|
}
|
|
|
|
// Frontend should NOT have hash label
|
|
fe := &nvidiacomv1alpha1.DynamoComponentDeployment{}
|
|
err = r.Get(ctx, types.NamespacedName{Name: "test-dgd-frontend", Namespace: "default"}, fe)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, fe.Labels[consts.KubeLabelDynamoWorkerHash],
|
|
"Frontend DCD should not have worker hash label")
|
|
}
|
|
|
|
func TestFindLegacyWorkerDCDs(t *testing.T) {
|
|
t.Run("finds worker DCDs without hash label", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
|
|
legacyDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, legacyDCD)
|
|
ctx := context.Background()
|
|
|
|
result, err := r.findLegacyWorkerDCDs(ctx, dgd)
|
|
require.NoError(t, err)
|
|
assert.Len(t, result, 1)
|
|
assert.Equal(t, "test-dgd-worker", result[0].Name)
|
|
})
|
|
|
|
t.Run("ignores non-worker DCDs", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"frontend": {ComponentType: consts.ComponentTypeFrontend},
|
|
})
|
|
|
|
frontendDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-frontend",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeFrontend,
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, frontendDCD)
|
|
ctx := context.Background()
|
|
|
|
result, err := r.findLegacyWorkerDCDs(ctx, dgd)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, result)
|
|
})
|
|
|
|
t.Run("ignores DCDs that already have hash label", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
|
|
hashedDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-abc12345",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: "abc12345",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, hashedDCD)
|
|
ctx := context.Background()
|
|
|
|
result, err := r.findLegacyWorkerDCDs(ctx, dgd)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, result)
|
|
})
|
|
|
|
t.Run("ignores DCDs from other DGDs", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
|
|
otherDGDWorkerDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "other-dgd-worker",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "other-dgd",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, otherDGDWorkerDCD)
|
|
ctx := context.Background()
|
|
|
|
result, err := r.findLegacyWorkerDCDs(ctx, dgd)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, result)
|
|
})
|
|
|
|
t.Run("no DCDs at all", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
|
|
r := createTestReconciler(dgd)
|
|
ctx := context.Background()
|
|
|
|
result, err := r.findLegacyWorkerDCDs(ctx, dgd)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, result)
|
|
})
|
|
}
|
|
|
|
func TestListOldWorkerDCDs(t *testing.T) {
|
|
t.Run("finds legacy DCDs as old", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
|
|
// Legacy DCD with backfilled "legacy" hash label
|
|
legacyDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: consts.LegacyWorkerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
ServiceName: "worker",
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, legacyDCD)
|
|
ctx := context.Background()
|
|
|
|
result, err := r.listOldWorkerDCDs(ctx, dgd, "newhash1")
|
|
require.NoError(t, err)
|
|
assert.Len(t, result, 1)
|
|
assert.Equal(t, "test-dgd-worker", result[0].Name)
|
|
})
|
|
|
|
t.Run("excludes current hash DCDs", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
|
|
currentDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-abc12345",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: "abc12345",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
ServiceName: "worker",
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, currentDCD)
|
|
ctx := context.Background()
|
|
|
|
result, err := r.listOldWorkerDCDs(ctx, dgd, "abc12345")
|
|
require.NoError(t, err)
|
|
assert.Empty(t, result)
|
|
})
|
|
|
|
t.Run("excludes non-worker DCDs", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"frontend": {ComponentType: consts.ComponentTypeFrontend},
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
|
|
// A frontend DCD with non-matching hash (should be excluded as non-worker)
|
|
frontendDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-frontend",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: testOldWorkerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeFrontend,
|
|
ServiceName: "frontend",
|
|
},
|
|
},
|
|
}
|
|
|
|
workerDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-oldhash1",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: testOldWorkerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
ServiceName: "worker",
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, frontendDCD, workerDCD)
|
|
ctx := context.Background()
|
|
|
|
result, err := r.listOldWorkerDCDs(ctx, dgd, testNewWorkerHash)
|
|
require.NoError(t, err)
|
|
assert.Len(t, result, 1)
|
|
assert.Equal(t, "test-dgd-worker-oldhash1", result[0].Name)
|
|
})
|
|
}
|
|
|
|
func TestScaleOldWorkerDCDs_LegacyDCDs(t *testing.T) {
|
|
t.Run("scales legacy-named DCD via label lookup", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
Replicas: ptr.To(int32(3)),
|
|
},
|
|
})
|
|
|
|
// Legacy DCD with backfilled hash label but old-style name (no hash suffix)
|
|
legacyDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: consts.LegacyWorkerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
ServiceName: "worker",
|
|
Replicas: ptr.To(int32(3)),
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, legacyDCD)
|
|
ctx := context.Background()
|
|
|
|
rollingUpdateCtx := dynamo.RollingUpdateContext{
|
|
NewWorkerHash: "newhash1",
|
|
OldWorkerReplicas: map[string]int32{"worker": 1},
|
|
NewWorkerReplicas: map[string]int32{"worker": 3},
|
|
}
|
|
|
|
err := r.scaleOldWorkerDCDs(ctx, dgd, rollingUpdateCtx)
|
|
require.NoError(t, err)
|
|
|
|
// Verify the legacy DCD was scaled down
|
|
updatedDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{}
|
|
err = r.Get(ctx, types.NamespacedName{Name: "test-dgd-worker", Namespace: "default"}, updatedDCD)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int32(1), *updatedDCD.Spec.Replicas, "Legacy DCD should be scaled to 1")
|
|
})
|
|
|
|
t.Run("no-op when rolling update not in progress", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
|
|
r := createTestReconciler(dgd)
|
|
ctx := context.Background()
|
|
|
|
// Empty OldWorkerReplicas = not in progress
|
|
rollingUpdateCtx := dynamo.RollingUpdateContext{
|
|
NewWorkerHash: "samehash",
|
|
OldWorkerReplicas: map[string]int32{},
|
|
NewWorkerReplicas: map[string]int32{},
|
|
}
|
|
|
|
err := r.scaleOldWorkerDCDs(ctx, dgd, rollingUpdateCtx)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("skips when replicas already at desired value", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
Replicas: ptr.To(int32(3)),
|
|
},
|
|
})
|
|
|
|
legacyDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: consts.LegacyWorkerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
ServiceName: "worker",
|
|
Replicas: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, legacyDCD)
|
|
ctx := context.Background()
|
|
|
|
rollingUpdateCtx := dynamo.RollingUpdateContext{
|
|
NewWorkerHash: "newhash1",
|
|
OldWorkerReplicas: map[string]int32{"worker": 1},
|
|
NewWorkerReplicas: map[string]int32{"worker": 3},
|
|
}
|
|
|
|
err := r.scaleOldWorkerDCDs(ctx, dgd, rollingUpdateCtx)
|
|
require.NoError(t, err)
|
|
|
|
// Replicas should remain at 1 (no patch needed)
|
|
updatedDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{}
|
|
err = r.Get(ctx, types.NamespacedName{Name: "test-dgd-worker", Namespace: "default"}, updatedDCD)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int32(1), *updatedDCD.Spec.Replicas)
|
|
})
|
|
}
|
|
|
|
func TestAggregateOldWorkerServiceStatuses_LegacyDCDs(t *testing.T) {
|
|
t.Run("aggregates status from legacy-named DCD", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
Replicas: ptr.To(int32(3)),
|
|
},
|
|
})
|
|
|
|
// Legacy DCD with old-style name but backfilled hash label
|
|
legacyDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: consts.LegacyWorkerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
ServiceName: "worker",
|
|
Replicas: ptr.To(int32(2)),
|
|
},
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
Service: &nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
ComponentKind: "Deployment",
|
|
ComponentName: "test-dgd-worker",
|
|
Replicas: 2,
|
|
ReadyReplicas: ptr.To(int32(2)),
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, legacyDCD)
|
|
ctx := context.Background()
|
|
|
|
rollingUpdateCtx := dynamo.RollingUpdateContext{
|
|
NewWorkerHash: "newhash1",
|
|
OldWorkerReplicas: map[string]int32{"worker": 2},
|
|
NewWorkerReplicas: map[string]int32{"worker": 3},
|
|
}
|
|
|
|
statuses, err := r.aggregateOldWorkerServiceStatuses(ctx, dgd, rollingUpdateCtx)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, statuses, 1)
|
|
assert.Equal(t, "test-dgd-worker", statuses["worker"].ComponentName)
|
|
assert.Equal(t, []string{"test-dgd-worker"}, statuses["worker"].ComponentNames)
|
|
assert.Equal(t, int32(2), statuses["worker"].Replicas)
|
|
assert.Equal(t, ptr.To(int32(2)), statuses["worker"].ReadyReplicas)
|
|
})
|
|
|
|
t.Run("no legacy DCDs found - returns empty", func(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
},
|
|
})
|
|
|
|
r := createTestReconciler(dgd)
|
|
ctx := context.Background()
|
|
|
|
rollingUpdateCtx := dynamo.RollingUpdateContext{
|
|
NewWorkerHash: "newhash1",
|
|
OldWorkerReplicas: map[string]int32{"worker": 1},
|
|
NewWorkerReplicas: map[string]int32{"worker": 1},
|
|
}
|
|
|
|
statuses, err := r.aggregateOldWorkerServiceStatuses(ctx, dgd, rollingUpdateCtx)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, statuses)
|
|
})
|
|
}
|
|
|
|
func TestDeleteOldWorkerDCDs_LegacyDCDs(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
|
|
// Legacy DCD with backfilled hash label
|
|
legacyDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: consts.LegacyWorkerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
},
|
|
},
|
|
}
|
|
|
|
// New DCD with real hash (should NOT be deleted)
|
|
newDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-abc12345",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: "abc12345",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, legacyDCD, newDCD)
|
|
ctx := context.Background()
|
|
|
|
err := r.deleteOldWorkerDCDs(ctx, dgd, "abc12345")
|
|
require.NoError(t, err)
|
|
|
|
// Verify legacy DCD is deleted and new DCD remains
|
|
dcdList := &nvidiacomv1alpha1.DynamoComponentDeploymentList{}
|
|
err = r.List(ctx, dcdList)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, dcdList.Items, 1)
|
|
assert.Equal(t, "test-dgd-worker-abc12345", dcdList.Items[0].Name)
|
|
}
|
|
|
|
func TestDeleteOldWorkerDCDs_MultipleGenerations(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
|
|
// Generation A (legacy)
|
|
legacyDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: consts.LegacyWorkerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Generation B (intermediate)
|
|
genBDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-hashbbbb",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: "hashbbbb",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Generation C (current)
|
|
currentDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-hashcccc",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: "hashcccc",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, legacyDCD, genBDCD, currentDCD)
|
|
ctx := context.Background()
|
|
|
|
err := r.deleteOldWorkerDCDs(ctx, dgd, "hashcccc")
|
|
require.NoError(t, err)
|
|
|
|
// Verify both old generations are deleted, only current remains
|
|
dcdList := &nvidiacomv1alpha1.DynamoComponentDeploymentList{}
|
|
err = r.List(ctx, dcdList)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, dcdList.Items, 1)
|
|
assert.Equal(t, "test-dgd-worker-hashcccc", dcdList.Items[0].Name)
|
|
}
|
|
|
|
func TestListOldWorkerDCDs_ExcludesCurrentHash(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
|
|
// Generation A
|
|
genADCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-hashaaaa",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: "hashaaaa",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
ServiceName: "worker",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Generation B
|
|
genBDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-hashbbbb",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: "hashbbbb",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
ServiceName: "worker",
|
|
},
|
|
},
|
|
}
|
|
|
|
// Generation C (current)
|
|
genCDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-hashcccc",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: "hashcccc",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
ServiceName: "worker",
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, genADCD, genBDCD, genCDCD)
|
|
ctx := context.Background()
|
|
|
|
result, err := r.listOldWorkerDCDs(ctx, dgd, "hashcccc")
|
|
require.NoError(t, err)
|
|
assert.Len(t, result, 2)
|
|
|
|
names := []string{result[0].Name, result[1].Name}
|
|
sort.Strings(names)
|
|
assert.Equal(t, []string{"test-dgd-worker-hashaaaa", "test-dgd-worker-hashbbbb"}, names)
|
|
}
|
|
|
|
func TestScaleOldWorkerDCDs_MultipleOldGenerations(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
Replicas: ptr.To(int32(4)),
|
|
},
|
|
})
|
|
|
|
now := metav1.Now()
|
|
earlier := metav1.NewTime(now.Add(-1 * 60 * 1e9)) // 1 minute earlier
|
|
|
|
// Generation A (oldest)
|
|
genADCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-hashaaaa",
|
|
Namespace: "default",
|
|
CreationTimestamp: earlier,
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: "hashaaaa",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
ServiceName: "worker",
|
|
Replicas: ptr.To(int32(2)),
|
|
},
|
|
},
|
|
}
|
|
|
|
// Generation B (newer old)
|
|
genBDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-hashbbbb",
|
|
Namespace: "default",
|
|
CreationTimestamp: now,
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: "hashbbbb",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
ServiceName: "worker",
|
|
Replicas: ptr.To(int32(2)),
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, genADCD, genBDCD)
|
|
ctx := context.Background()
|
|
|
|
// oldNeeded = 2: newest old (B) should get 2, oldest (A) should get 0
|
|
rollingUpdateCtx := dynamo.RollingUpdateContext{
|
|
NewWorkerHash: "hashcccc",
|
|
OldWorkerReplicas: map[string]int32{"worker": 2},
|
|
NewWorkerReplicas: map[string]int32{"worker": 4},
|
|
}
|
|
|
|
err := r.scaleOldWorkerDCDs(ctx, dgd, rollingUpdateCtx)
|
|
require.NoError(t, err)
|
|
|
|
// Newest old (B) should keep replicas (up to 2)
|
|
updatedB := &nvidiacomv1alpha1.DynamoComponentDeployment{}
|
|
err = r.Get(ctx, types.NamespacedName{Name: "test-dgd-worker-hashbbbb", Namespace: "default"}, updatedB)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int32(2), *updatedB.Spec.Replicas, "Newest old DCD should have 2 replicas")
|
|
|
|
// Oldest (A) should be drained to 0
|
|
updatedA := &nvidiacomv1alpha1.DynamoComponentDeployment{}
|
|
err = r.Get(ctx, types.NamespacedName{Name: "test-dgd-worker-hashaaaa", Namespace: "default"}, updatedA)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, int32(0), *updatedA.Spec.Replicas, "Oldest old DCD should be drained to 0")
|
|
}
|
|
|
|
func TestAggregateOldWorkerServiceStatuses_MultipleOldGenerations(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
Replicas: ptr.To(int32(4)),
|
|
},
|
|
})
|
|
|
|
// Generation A
|
|
genADCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-hashaaaa",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: "hashaaaa",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
ServiceName: "worker",
|
|
Replicas: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
Service: &nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
ComponentKind: "Deployment",
|
|
ComponentName: "test-dgd-worker-hashaaaa",
|
|
Replicas: 1,
|
|
ReadyReplicas: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
}
|
|
|
|
// Generation B
|
|
genBDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-hashbbbb",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: "hashbbbb",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
ServiceName: "worker",
|
|
Replicas: ptr.To(int32(2)),
|
|
},
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
Service: &nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
ComponentKind: "Deployment",
|
|
ComponentName: "test-dgd-worker-hashbbbb",
|
|
Replicas: 2,
|
|
ReadyReplicas: ptr.To(int32(2)),
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconciler(dgd, genADCD, genBDCD)
|
|
ctx := context.Background()
|
|
|
|
rollingUpdateCtx := dynamo.RollingUpdateContext{
|
|
NewWorkerHash: "hashcccc",
|
|
OldWorkerReplicas: map[string]int32{"worker": 3},
|
|
NewWorkerReplicas: map[string]int32{"worker": 4},
|
|
}
|
|
|
|
statuses, err := r.aggregateOldWorkerServiceStatuses(ctx, dgd, rollingUpdateCtx)
|
|
require.NoError(t, err)
|
|
|
|
assert.Len(t, statuses, 1)
|
|
// Replicas should be summed across both old generations
|
|
assert.Equal(t, int32(3), statuses["worker"].Replicas)
|
|
assert.Equal(t, ptr.To(int32(3)), statuses["worker"].ReadyReplicas)
|
|
// ComponentNames should include both old DCDs
|
|
assert.Len(t, statuses["worker"].ComponentNames, 2)
|
|
}
|
|
|
|
func TestContinueRollingUpdate_CascadingSpecChange(t *testing.T) {
|
|
// Scenario: A→B rolling update in progress, spec changes to C.
|
|
// B DCDs should be treated as old alongside A DCDs.
|
|
newWorkerHash := "hashcccc"
|
|
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
Replicas: ptr.To(int32(2)),
|
|
},
|
|
})
|
|
dgd.Annotations = map[string]string{
|
|
consts.AnnotationCurrentWorkerHash: "hashaaaa",
|
|
}
|
|
dgd.Status.RollingUpdate = &nvidiacomv1alpha1.RollingUpdateStatus{
|
|
Phase: nvidiacomv1alpha1.RollingUpdatePhaseInProgress,
|
|
}
|
|
|
|
// Generation A (old)
|
|
genADCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-hashaaaa",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: "hashaaaa",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
ServiceName: "worker",
|
|
Replicas: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
Service: &nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
ReadyReplicas: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
}
|
|
|
|
// Generation B (intermediate, now also old)
|
|
genBDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-hashbbbb",
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: "hashbbbb",
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
ServiceName: "worker",
|
|
Replicas: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
Service: &nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
ReadyReplicas: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
}
|
|
|
|
// Generation C (new, not yet ready)
|
|
genCDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-" + newWorkerHash[:8],
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: newWorkerHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
ServiceName: "worker",
|
|
Replicas: ptr.To(int32(2)),
|
|
},
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
Service: &nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
ReadyReplicas: ptr.To(int32(0)),
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconcilerWithStatus(dgd, genADCD, genBDCD, genCDCD)
|
|
ctx := context.Background()
|
|
|
|
rollingUpdateStatus := dgd.Status.RollingUpdate
|
|
err := r.continueRollingUpdate(ctx, dgd, rollingUpdateStatus, newWorkerHash)
|
|
require.NoError(t, err)
|
|
|
|
// Both A and B have ready replicas, C has 0 — rolling update not complete
|
|
assert.Equal(t, nvidiacomv1alpha1.RollingUpdatePhaseInProgress, rollingUpdateStatus.Phase)
|
|
assert.Empty(t, rollingUpdateStatus.UpdatedServices, "No services should be fully updated yet")
|
|
}
|
|
|
|
func TestResolveRollingUpdateParams(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
annotations map[string]string
|
|
desiredReplicas int32
|
|
expectedSurge int32
|
|
expectedUnavail int32
|
|
}{
|
|
{
|
|
name: "defaults - no annotations - 25%/25% of 4 = 1/1",
|
|
annotations: nil,
|
|
desiredReplicas: 4,
|
|
expectedSurge: 1,
|
|
expectedUnavail: 1,
|
|
},
|
|
{
|
|
name: "absolute maxSurge overrides default",
|
|
annotations: map[string]string{
|
|
KubeAnnotationDeploymentRollingUpdateMaxSurge: "2",
|
|
},
|
|
desiredReplicas: 4,
|
|
expectedSurge: 2,
|
|
expectedUnavail: 1,
|
|
},
|
|
{
|
|
name: "absolute maxUnavailable overrides default",
|
|
annotations: map[string]string{
|
|
KubeAnnotationDeploymentRollingUpdateMaxUnavailable: "0",
|
|
},
|
|
desiredReplicas: 4,
|
|
expectedSurge: 1,
|
|
expectedUnavail: 0,
|
|
},
|
|
{
|
|
name: "percentage maxSurge - 50% of 4 = 2",
|
|
annotations: map[string]string{
|
|
KubeAnnotationDeploymentRollingUpdateMaxSurge: "50%",
|
|
},
|
|
desiredReplicas: 4,
|
|
expectedSurge: 2,
|
|
expectedUnavail: 1,
|
|
},
|
|
{
|
|
name: "percentage maxUnavailable - 50% of 4 = 2",
|
|
annotations: map[string]string{
|
|
KubeAnnotationDeploymentRollingUpdateMaxUnavailable: "50%",
|
|
},
|
|
desiredReplicas: 4,
|
|
expectedSurge: 1,
|
|
expectedUnavail: 2,
|
|
},
|
|
{
|
|
name: "both annotations set with percentages",
|
|
annotations: map[string]string{
|
|
KubeAnnotationDeploymentRollingUpdateMaxSurge: "50%",
|
|
KubeAnnotationDeploymentRollingUpdateMaxUnavailable: "25%",
|
|
},
|
|
desiredReplicas: 4,
|
|
expectedSurge: 2,
|
|
expectedUnavail: 1,
|
|
},
|
|
{
|
|
name: "both zero - force surge to 1 for progress",
|
|
annotations: map[string]string{
|
|
KubeAnnotationDeploymentRollingUpdateMaxSurge: "0",
|
|
KubeAnnotationDeploymentRollingUpdateMaxUnavailable: "0",
|
|
},
|
|
desiredReplicas: 4,
|
|
expectedSurge: 1,
|
|
expectedUnavail: 0,
|
|
},
|
|
{
|
|
name: "maxSurge 0 with maxUnavailable 1 - allowed",
|
|
annotations: map[string]string{
|
|
KubeAnnotationDeploymentRollingUpdateMaxSurge: "0",
|
|
KubeAnnotationDeploymentRollingUpdateMaxUnavailable: "1",
|
|
},
|
|
desiredReplicas: 4,
|
|
expectedSurge: 0,
|
|
expectedUnavail: 1,
|
|
},
|
|
{
|
|
name: "percentage surge rounds up - 34% of 3 rounds up to 2",
|
|
annotations: map[string]string{
|
|
KubeAnnotationDeploymentRollingUpdateMaxSurge: "34%",
|
|
},
|
|
desiredReplicas: 3,
|
|
expectedSurge: 2,
|
|
expectedUnavail: 0,
|
|
},
|
|
{
|
|
name: "percentage unavailable rounds down - 34% of 3 rounds down to 1",
|
|
annotations: map[string]string{
|
|
KubeAnnotationDeploymentRollingUpdateMaxUnavailable: "34%",
|
|
},
|
|
desiredReplicas: 3,
|
|
expectedSurge: 1,
|
|
expectedUnavail: 1,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
surge, unavail := resolveRollingUpdateParams(tt.annotations, tt.desiredReplicas)
|
|
assert.Equal(t, tt.expectedSurge, surge, "maxSurge")
|
|
assert.Equal(t, tt.expectedUnavail, unavail, "maxUnavailable")
|
|
})
|
|
}
|
|
}
|
|
|
|
// --- reconcileRollingUpdate state machine tests ---
|
|
|
|
func TestReconcileRollingUpdate_NoChange(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
hash := dynamo.ComputeDGDWorkersSpecHash(dgd)
|
|
dgd.Annotations = map[string]string{consts.AnnotationCurrentWorkerHash: hash}
|
|
dgd.Status.RollingUpdate = &nvidiacomv1alpha1.RollingUpdateStatus{
|
|
Phase: nvidiacomv1alpha1.RollingUpdatePhaseCompleted,
|
|
}
|
|
|
|
r := createTestReconcilerWithStatus(dgd)
|
|
err := r.reconcileRollingUpdate(context.Background(), dgd)
|
|
require.NoError(t, err)
|
|
// Phase should stay Completed — no spec change
|
|
assert.Equal(t, nvidiacomv1alpha1.RollingUpdatePhaseCompleted, dgd.Status.RollingUpdate.Phase)
|
|
}
|
|
|
|
func TestReconcileRollingUpdate_SpecChangeStartsRollout(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
dgd.Annotations = map[string]string{consts.AnnotationCurrentWorkerHash: "stale000"}
|
|
dgd.Status.RollingUpdate = &nvidiacomv1alpha1.RollingUpdateStatus{
|
|
Phase: nvidiacomv1alpha1.RollingUpdatePhaseCompleted,
|
|
}
|
|
|
|
r := createTestReconcilerWithStatus(dgd)
|
|
err := r.reconcileRollingUpdate(context.Background(), dgd)
|
|
require.NoError(t, err)
|
|
// Should transition to Pending (new rollout started)
|
|
assert.Equal(t, nvidiacomv1alpha1.RollingUpdatePhasePending, dgd.Status.RollingUpdate.Phase)
|
|
assert.NotNil(t, dgd.Status.RollingUpdate.StartTime)
|
|
}
|
|
|
|
func TestReconcileRollingUpdate_PendingToInProgress(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
dgd.Annotations = map[string]string{consts.AnnotationCurrentWorkerHash: "oldhash0"}
|
|
dgd.Status.RollingUpdate = &nvidiacomv1alpha1.RollingUpdateStatus{
|
|
Phase: nvidiacomv1alpha1.RollingUpdatePhasePending,
|
|
}
|
|
|
|
r := createTestReconcilerWithStatus(dgd)
|
|
err := r.reconcileRollingUpdate(context.Background(), dgd)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, nvidiacomv1alpha1.RollingUpdatePhaseInProgress, dgd.Status.RollingUpdate.Phase)
|
|
}
|
|
|
|
func TestReconcileRollingUpdate_StuckDetection(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
hash := dynamo.ComputeDGDWorkersSpecHash(dgd)
|
|
// Hash matches current but phase is InProgress — stuck
|
|
dgd.Annotations = map[string]string{consts.AnnotationCurrentWorkerHash: hash}
|
|
dgd.Status.RollingUpdate = &nvidiacomv1alpha1.RollingUpdateStatus{
|
|
Phase: nvidiacomv1alpha1.RollingUpdatePhaseInProgress,
|
|
}
|
|
|
|
r := createTestReconcilerWithStatus(dgd)
|
|
err := r.reconcileRollingUpdate(context.Background(), dgd)
|
|
require.NoError(t, err)
|
|
// Should auto-complete
|
|
assert.Equal(t, nvidiacomv1alpha1.RollingUpdatePhaseCompleted, dgd.Status.RollingUpdate.Phase)
|
|
}
|
|
|
|
func TestReconcileRollingUpdate_NewRollingUpdate(t *testing.T) {
|
|
newHash := "newhash1"
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
dgd.Annotations = map[string]string{consts.AnnotationCurrentWorkerHash: "oldhash0"}
|
|
dgd.Status.RollingUpdate = &nvidiacomv1alpha1.RollingUpdateStatus{
|
|
Phase: nvidiacomv1alpha1.RollingUpdatePhaseCompleted,
|
|
}
|
|
|
|
// Create a DCD with the new hash that has ready replicas — stale annotation scenario
|
|
newDCD := &nvidiacomv1alpha1.DynamoComponentDeployment{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "test-dgd-worker-" + newHash,
|
|
Namespace: "default",
|
|
Labels: map[string]string{
|
|
consts.KubeLabelDynamoGraphDeploymentName: "test-dgd",
|
|
consts.KubeLabelDynamoWorkerHash: newHash,
|
|
},
|
|
},
|
|
Spec: nvidiacomv1alpha1.DynamoComponentDeploymentSpec{
|
|
DynamoComponentDeploymentSharedSpec: nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
ComponentType: consts.ComponentTypeWorker,
|
|
ServiceName: "worker",
|
|
},
|
|
},
|
|
Status: nvidiacomv1alpha1.DynamoComponentDeploymentStatus{
|
|
Service: &nvidiacomv1alpha1.ServiceReplicaStatus{
|
|
ReadyReplicas: ptr.To(int32(1)),
|
|
},
|
|
},
|
|
}
|
|
|
|
r := createTestReconcilerWithStatus(dgd, newDCD)
|
|
|
|
// When computed hash != current hash and no DCDs exist with computed hash, start rollout.
|
|
err := r.reconcileRollingUpdate(context.Background(), dgd)
|
|
require.NoError(t, err)
|
|
// Should start a new rolling update (Pending) since computed hash DCDs don't exist
|
|
assert.Equal(t, nvidiacomv1alpha1.RollingUpdatePhasePending, dgd.Status.RollingUpdate.Phase)
|
|
}
|
|
|
|
func TestReconcileRollingUpdate_NonePhaseStartsRollout(t *testing.T) {
|
|
dgd := createTestDGD("test-dgd", map[string]*nvidiacomv1alpha1.DynamoComponentDeploymentSharedSpec{
|
|
"worker": {ComponentType: consts.ComponentTypeWorker},
|
|
})
|
|
dgd.Annotations = map[string]string{consts.AnnotationCurrentWorkerHash: "oldhash0"}
|
|
dgd.Status.RollingUpdate = &nvidiacomv1alpha1.RollingUpdateStatus{
|
|
Phase: nvidiacomv1alpha1.RollingUpdatePhaseNone,
|
|
}
|
|
|
|
r := createTestReconcilerWithStatus(dgd)
|
|
err := r.reconcileRollingUpdate(context.Background(), dgd)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, nvidiacomv1alpha1.RollingUpdatePhasePending, dgd.Status.RollingUpdate.Phase)
|
|
assert.NotNil(t, dgd.Status.RollingUpdate.StartTime)
|
|
assert.Nil(t, dgd.Status.RollingUpdate.UpdatedServices)
|
|
}
|