/* Copyright 2024 The Karmada Authors. 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 deploymentreplicassyncer import ( "context" "testing" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/reconcile" policyv1alpha1 "github.com/karmada-io/karmada/pkg/apis/policy/v1alpha1" workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2" "github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util/names" ) // predicateTestCase defines the structure for testing predicate functions type predicateTestCase struct { name string oldObj *appsv1.Deployment newObj *appsv1.Deployment expected bool eventType string } func TestPredicateFunc(t *testing.T) { testCases := []predicateTestCase{ { name: "retain-replicas label added", oldObj: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{}, }, }, newObj: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{util.RetainReplicasLabel: util.RetainReplicasValue}, }, }, expected: true, eventType: "update", }, { name: "replicas changed", oldObj: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{util.RetainReplicasLabel: util.RetainReplicasValue}, }, Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](3), }, Status: appsv1.DeploymentStatus{ Replicas: 3, }, }, newObj: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{util.RetainReplicasLabel: util.RetainReplicasValue}, }, Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](3), }, Status: appsv1.DeploymentStatus{ Replicas: 4, }, }, expected: true, eventType: "update", }, { name: "no relevant changes", oldObj: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{util.RetainReplicasLabel: util.RetainReplicasValue}, }, Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](3), }, Status: appsv1.DeploymentStatus{ Replicas: 3, }, }, newObj: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{util.RetainReplicasLabel: util.RetainReplicasValue}, }, Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](3), }, Status: appsv1.DeploymentStatus{ Replicas: 3, }, }, expected: false, eventType: "update", }, { name: "create event test", oldObj: nil, newObj: &appsv1.Deployment{}, expected: false, eventType: "create", }, { name: "delete event test", oldObj: &appsv1.Deployment{}, newObj: nil, expected: false, eventType: "delete", }, { name: "generic event test", oldObj: nil, newObj: &appsv1.Deployment{}, expected: false, eventType: "generic", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { var result bool // Handle different event types using the eventType field switch tc.eventType { case "create": result = predicateFunc.Create(event.CreateEvent{Object: tc.newObj}) case "delete": result = predicateFunc.Delete(event.DeleteEvent{Object: tc.oldObj}) case "generic": result = predicateFunc.Generic(event.GenericEvent{Object: tc.newObj}) case "update": result = predicateFunc.Update(event.UpdateEvent{ ObjectOld: tc.oldObj, ObjectNew: tc.newObj, }) } assert.Equal(t, tc.expected, result) }) } } func TestReconcile(t *testing.T) { testCases := []struct { name string deployment *appsv1.Deployment binding *workv1alpha2.ResourceBinding expectedResult reconcile.Result expectedError bool expectedReplicas *int32 }{ { name: "successful replicas sync", deployment: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "test-deployment", Namespace: "default", Labels: map[string]string{util.RetainReplicasLabel: util.RetainReplicasValue}, }, Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](3), }, Status: appsv1.DeploymentStatus{ Replicas: 4, }, }, binding: &workv1alpha2.ResourceBinding{ ObjectMeta: metav1.ObjectMeta{ Name: names.GenerateBindingName(util.DeploymentKind, "test-deployment"), Namespace: "default", Generation: 1, }, Spec: workv1alpha2.ResourceBindingSpec{ Replicas: 3, Placement: &policyv1alpha1.Placement{ ReplicaScheduling: &policyv1alpha1.ReplicaSchedulingStrategy{ ReplicaSchedulingType: policyv1alpha1.ReplicaSchedulingTypeDivided, }, }, Clusters: []workv1alpha2.TargetCluster{ {Name: "cluster1"}, {Name: "cluster2"}, }, }, Status: workv1alpha2.ResourceBindingStatus{ SchedulerObservedGeneration: 1, AggregatedStatus: []workv1alpha2.AggregatedStatusItem{ { ClusterName: "cluster1", Status: &runtime.RawExtension{ Raw: []byte(`{"observedGeneration":1,"replicas":3,"updatedReplicas":3,"readyReplicas":3,"availableReplicas":3}`), }, }, { ClusterName: "cluster2", Status: &runtime.RawExtension{ Raw: []byte(`{"observedGeneration":1,"replicas":1,"updatedReplicas":1,"readyReplicas":1,"availableReplicas":1}`), }, }, }, }, }, expectedResult: reconcile.Result{}, expectedError: false, expectedReplicas: ptr.To[int32](4), }, { name: "binding not found", deployment: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "test-deployment", Namespace: "default", Labels: map[string]string{util.RetainReplicasLabel: util.RetainReplicasValue}, }, }, expectedResult: reconcile.Result{}, expectedError: false, }, { name: "non-divided scheduling type", deployment: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "test-deployment", Namespace: "default", Labels: map[string]string{util.RetainReplicasLabel: util.RetainReplicasValue}, }, }, binding: &workv1alpha2.ResourceBinding{ ObjectMeta: metav1.ObjectMeta{ Name: names.GenerateBindingName(util.DeploymentKind, "test-deployment"), Namespace: "default", }, Spec: workv1alpha2.ResourceBindingSpec{ Placement: &policyv1alpha1.Placement{}, }, }, expectedResult: reconcile.Result{}, expectedError: false, }, { name: "replicas already synced", deployment: &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "test-deployment", Namespace: "default", Labels: map[string]string{util.RetainReplicasLabel: util.RetainReplicasValue}, }, Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](3), }, Status: appsv1.DeploymentStatus{ Replicas: 3, }, }, binding: &workv1alpha2.ResourceBinding{ ObjectMeta: metav1.ObjectMeta{ Name: names.GenerateBindingName(util.DeploymentKind, "test-deployment"), Namespace: "default", }, Spec: workv1alpha2.ResourceBindingSpec{ Replicas: 3, Placement: &policyv1alpha1.Placement{ ReplicaScheduling: &policyv1alpha1.ReplicaSchedulingStrategy{ ReplicaSchedulingType: policyv1alpha1.ReplicaSchedulingTypeDivided, }, }, }, }, expectedResult: reconcile.Result{}, expectedError: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { scheme := runtime.NewScheme() _ = appsv1.AddToScheme(scheme) _ = workv1alpha2.Install(scheme) var objs []runtime.Object if tc.deployment != nil { objs = append(objs, tc.deployment) } if tc.binding != nil { objs = append(objs, tc.binding) } client := fake.NewClientBuilder(). WithScheme(scheme). WithRuntimeObjects(objs...). WithStatusSubresource(&appsv1.Deployment{}). Build() r := &DeploymentReplicasSyncer{Client: client} req := reconcile.Request{ NamespacedName: types.NamespacedName{ Name: "test-deployment", Namespace: "default", }, } result, err := r.Reconcile(context.TODO(), req) if tc.expectedError { assert.Error(t, err) } else { assert.NoError(t, err) } assert.Equal(t, tc.expectedResult, result) if tc.expectedReplicas != nil { updatedDeployment := &appsv1.Deployment{} err := client.Get(context.TODO(), types.NamespacedName{Name: "test-deployment", Namespace: "default"}, updatedDeployment) assert.NoError(t, err) assert.Equal(t, *tc.expectedReplicas, *updatedDeployment.Spec.Replicas) } }) } } func TestIsDeploymentStatusCollected(t *testing.T) { testCases := []struct { name string deployment *appsv1.Deployment binding *workv1alpha2.ResourceBinding expected bool }{ { name: "status fully collected", deployment: &appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](3), }, Status: appsv1.DeploymentStatus{ Replicas: 3, }, }, binding: &workv1alpha2.ResourceBinding{ ObjectMeta: metav1.ObjectMeta{ Generation: 1, }, Spec: workv1alpha2.ResourceBindingSpec{ Replicas: 3, Clusters: []workv1alpha2.TargetCluster{ {Name: "cluster1"}, {Name: "cluster2"}, }, }, Status: workv1alpha2.ResourceBindingStatus{ SchedulerObservedGeneration: 1, AggregatedStatus: []workv1alpha2.AggregatedStatusItem{ { ClusterName: "cluster1", Status: &runtime.RawExtension{Raw: []byte(`{"replicas": 2}`)}, }, { ClusterName: "cluster2", Status: &runtime.RawExtension{Raw: []byte(`{"replicas": 1}`)}, }, }, }, }, expected: true, }, { name: "binding replicas not matching deployment spec", deployment: &appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](3), }, Status: appsv1.DeploymentStatus{ Replicas: 3, }, }, binding: &workv1alpha2.ResourceBinding{ Spec: workv1alpha2.ResourceBindingSpec{ Replicas: 2, }, }, expected: false, }, { name: "scheduler observed generation not matching", deployment: &appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](3), }, Status: appsv1.DeploymentStatus{ Replicas: 3, }, }, binding: &workv1alpha2.ResourceBinding{ ObjectMeta: metav1.ObjectMeta{ Generation: 2, }, Spec: workv1alpha2.ResourceBindingSpec{ Replicas: 3, }, Status: workv1alpha2.ResourceBindingStatus{ SchedulerObservedGeneration: 1, }, }, expected: false, }, { name: "incomplete aggregated status", deployment: &appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](3), }, Status: appsv1.DeploymentStatus{ Replicas: 3, }, }, binding: &workv1alpha2.ResourceBinding{ ObjectMeta: metav1.ObjectMeta{ Generation: 1, }, Spec: workv1alpha2.ResourceBindingSpec{ Replicas: 3, Clusters: []workv1alpha2.TargetCluster{ {Name: "cluster1"}, {Name: "cluster2"}, }, }, Status: workv1alpha2.ResourceBindingStatus{ SchedulerObservedGeneration: 1, AggregatedStatus: []workv1alpha2.AggregatedStatusItem{ { ClusterName: "cluster1", Status: &runtime.RawExtension{Raw: []byte(`{"replicas": 2}`)}, }, }, }, }, expected: false, }, { name: "aggregated status is nil", deployment: &appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](3), }, Status: appsv1.DeploymentStatus{ Replicas: 3, }, }, binding: &workv1alpha2.ResourceBinding{ ObjectMeta: metav1.ObjectMeta{ Generation: 1, }, Spec: workv1alpha2.ResourceBindingSpec{ Replicas: 3, Clusters: []workv1alpha2.TargetCluster{ {Name: "cluster1"}, }, }, Status: workv1alpha2.ResourceBindingStatus{ SchedulerObservedGeneration: 1, AggregatedStatus: []workv1alpha2.AggregatedStatusItem{ { ClusterName: "cluster1", Status: nil, }, }, }, }, expected: false, }, { name: "invalid status JSON", deployment: &appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](3), }, Status: appsv1.DeploymentStatus{ Replicas: 3, }, }, binding: &workv1alpha2.ResourceBinding{ ObjectMeta: metav1.ObjectMeta{ Generation: 1, }, Spec: workv1alpha2.ResourceBindingSpec{ Replicas: 3, Clusters: []workv1alpha2.TargetCluster{ {Name: "cluster1"}, }, }, Status: workv1alpha2.ResourceBindingStatus{ SchedulerObservedGeneration: 1, AggregatedStatus: []workv1alpha2.AggregatedStatusItem{ { ClusterName: "cluster1", Status: &runtime.RawExtension{Raw: []byte(`invalid json`)}, }, }, }, }, expected: false, }, { name: "zero replicas in status", deployment: &appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](3), }, Status: appsv1.DeploymentStatus{ Replicas: 3, }, }, binding: &workv1alpha2.ResourceBinding{ ObjectMeta: metav1.ObjectMeta{ Generation: 1, }, Spec: workv1alpha2.ResourceBindingSpec{ Replicas: 3, Clusters: []workv1alpha2.TargetCluster{ {Name: "cluster1"}, }, }, Status: workv1alpha2.ResourceBindingStatus{ SchedulerObservedGeneration: 1, AggregatedStatus: []workv1alpha2.AggregatedStatusItem{ { ClusterName: "cluster1", Status: &runtime.RawExtension{Raw: []byte(`{"replicas": 0}`)}, }, }, }, }, expected: false, }, { name: "binding status replicas not matching deployment status", deployment: &appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Replicas: ptr.To[int32](3), }, Status: appsv1.DeploymentStatus{ Replicas: 3, }, }, binding: &workv1alpha2.ResourceBinding{ ObjectMeta: metav1.ObjectMeta{ Generation: 1, }, Spec: workv1alpha2.ResourceBindingSpec{ Replicas: 3, Clusters: []workv1alpha2.TargetCluster{ {Name: "cluster1"}, }, }, Status: workv1alpha2.ResourceBindingStatus{ SchedulerObservedGeneration: 1, AggregatedStatus: []workv1alpha2.AggregatedStatusItem{ { ClusterName: "cluster1", Status: &runtime.RawExtension{Raw: []byte(`{"replicas": 2}`)}, }, }, }, }, expected: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := isDeploymentStatusCollected(tc.deployment, tc.binding) assert.Equal(t, tc.expected, result) }) } }