Skip to content

Commit f19a4b5

Browse files
authored
Merge pull request #5543 from anujagrawal699/addedTests-controllers/gracefuleviction
Added tests for graceful eviction controller
2 parents 13df63f + 547a6a9 commit f19a4b5

File tree

3 files changed

+591
-0
lines changed

3 files changed

+591
-0
lines changed
Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
/*
2+
Copyright 2024 The Karmada Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package gracefuleviction
18+
19+
import (
20+
"context"
21+
"encoding/json"
22+
"math"
23+
"testing"
24+
"time"
25+
26+
"github.com/stretchr/testify/assert"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/apimachinery/pkg/runtime"
29+
"k8s.io/apimachinery/pkg/types"
30+
"k8s.io/client-go/tools/record"
31+
controllerruntime "sigs.k8s.io/controller-runtime"
32+
"sigs.k8s.io/controller-runtime/pkg/client"
33+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
34+
35+
workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
36+
"github.com/karmada-io/karmada/pkg/sharedcli/ratelimiterflag"
37+
)
38+
39+
func TestCRBGracefulEvictionController_Reconcile(t *testing.T) {
40+
scheme := runtime.NewScheme()
41+
err := workv1alpha2.Install(scheme)
42+
assert.NoError(t, err, "Failed to add workv1alpha2 to scheme")
43+
now := metav1.Now()
44+
testCases := []struct {
45+
name string
46+
binding *workv1alpha2.ClusterResourceBinding
47+
expectedResult controllerruntime.Result
48+
expectedError bool
49+
expectedRequeue bool
50+
notFound bool
51+
}{
52+
{
53+
name: "binding with no graceful eviction tasks",
54+
binding: &workv1alpha2.ClusterResourceBinding{
55+
ObjectMeta: metav1.ObjectMeta{
56+
Name: "test-binding",
57+
},
58+
Spec: workv1alpha2.ResourceBindingSpec{
59+
GracefulEvictionTasks: []workv1alpha2.GracefulEvictionTask{},
60+
},
61+
},
62+
expectedResult: controllerruntime.Result{},
63+
expectedError: false,
64+
expectedRequeue: false,
65+
},
66+
{
67+
name: "binding with active graceful eviction tasks",
68+
binding: &workv1alpha2.ClusterResourceBinding{
69+
ObjectMeta: metav1.ObjectMeta{
70+
Name: "test-binding",
71+
},
72+
Spec: workv1alpha2.ResourceBindingSpec{
73+
GracefulEvictionTasks: []workv1alpha2.GracefulEvictionTask{
74+
{
75+
FromCluster: "cluster1",
76+
CreationTimestamp: &now,
77+
},
78+
},
79+
},
80+
Status: workv1alpha2.ResourceBindingStatus{
81+
SchedulerObservedGeneration: 1,
82+
},
83+
},
84+
expectedResult: controllerruntime.Result{},
85+
expectedError: false,
86+
expectedRequeue: false,
87+
},
88+
{
89+
name: "binding marked for deletion",
90+
binding: &workv1alpha2.ClusterResourceBinding{
91+
ObjectMeta: metav1.ObjectMeta{
92+
Name: "test-binding",
93+
DeletionTimestamp: &now,
94+
Finalizers: []string{"test-finalizer"},
95+
},
96+
},
97+
expectedResult: controllerruntime.Result{},
98+
expectedError: false,
99+
expectedRequeue: false,
100+
},
101+
{
102+
name: "binding not found",
103+
binding: &workv1alpha2.ClusterResourceBinding{
104+
ObjectMeta: metav1.ObjectMeta{
105+
Name: "non-existent-binding",
106+
},
107+
},
108+
expectedResult: controllerruntime.Result{},
109+
expectedError: false,
110+
expectedRequeue: false,
111+
notFound: true,
112+
},
113+
}
114+
for _, tc := range testCases {
115+
t.Run(tc.name, func(t *testing.T) {
116+
// Create a fake client with or without the binding object
117+
var client client.Client
118+
if tc.notFound {
119+
client = fake.NewClientBuilder().WithScheme(scheme).Build()
120+
} else {
121+
client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(tc.binding).Build()
122+
}
123+
c := &CRBGracefulEvictionController{
124+
Client: client,
125+
EventRecorder: record.NewFakeRecorder(10),
126+
RateLimiterOptions: ratelimiterflag.Options{},
127+
GracefulEvictionTimeout: 5 * time.Minute,
128+
}
129+
result, err := c.Reconcile(context.TODO(), controllerruntime.Request{
130+
NamespacedName: types.NamespacedName{
131+
Name: tc.binding.Name,
132+
},
133+
})
134+
if tc.expectedError {
135+
assert.Error(t, err)
136+
} else {
137+
assert.NoError(t, err)
138+
}
139+
assert.Equal(t, tc.expectedResult, result)
140+
if tc.expectedRequeue {
141+
assert.True(t, result.RequeueAfter > 0, "Expected requeue, but got no requeue")
142+
} else {
143+
assert.Zero(t, result.RequeueAfter, "Expected no requeue, but got requeue")
144+
}
145+
// Verify the binding was updated, unless it's the "not found" case
146+
if !tc.notFound {
147+
updatedBinding := &workv1alpha2.ClusterResourceBinding{}
148+
err = client.Get(context.TODO(), types.NamespacedName{Name: tc.binding.Name}, updatedBinding)
149+
assert.NoError(t, err)
150+
}
151+
})
152+
}
153+
}
154+
155+
func TestCRBGracefulEvictionController_syncBinding(t *testing.T) {
156+
now := metav1.Now()
157+
timeout := 5 * time.Minute
158+
159+
s := runtime.NewScheme()
160+
err := workv1alpha2.Install(s)
161+
assert.NoError(t, err, "Failed to add workv1alpha2 to scheme")
162+
163+
tests := []struct {
164+
name string
165+
binding *workv1alpha2.ClusterResourceBinding
166+
expectedRetryAfter time.Duration
167+
expectedEvictionLen int
168+
expectedError bool
169+
}{
170+
{
171+
name: "no tasks",
172+
binding: &workv1alpha2.ClusterResourceBinding{
173+
ObjectMeta: metav1.ObjectMeta{
174+
Name: "test-binding",
175+
},
176+
Spec: workv1alpha2.ResourceBindingSpec{
177+
Resource: workv1alpha2.ObjectReference{
178+
APIVersion: "apps/v1",
179+
Kind: "Deployment",
180+
Name: "test-deployment",
181+
},
182+
Clusters: []workv1alpha2.TargetCluster{
183+
{Name: "cluster1"},
184+
},
185+
},
186+
},
187+
expectedRetryAfter: 0,
188+
expectedEvictionLen: 0,
189+
expectedError: false,
190+
},
191+
{
192+
name: "task not expired",
193+
binding: &workv1alpha2.ClusterResourceBinding{
194+
ObjectMeta: metav1.ObjectMeta{
195+
Name: "test-binding",
196+
},
197+
Spec: workv1alpha2.ResourceBindingSpec{
198+
Resource: workv1alpha2.ObjectReference{
199+
APIVersion: "apps/v1",
200+
Kind: "Deployment",
201+
Name: "test-deployment",
202+
},
203+
Clusters: []workv1alpha2.TargetCluster{
204+
{Name: "cluster1"},
205+
},
206+
GracefulEvictionTasks: []workv1alpha2.GracefulEvictionTask{
207+
{
208+
FromCluster: "cluster1",
209+
CreationTimestamp: &metav1.Time{Time: now.Add(-2 * time.Minute)},
210+
},
211+
},
212+
},
213+
Status: workv1alpha2.ResourceBindingStatus{
214+
AggregatedStatus: []workv1alpha2.AggregatedStatusItem{
215+
{
216+
ClusterName: "cluster1",
217+
Status: createRawExtension("Bound"),
218+
},
219+
},
220+
},
221+
},
222+
expectedRetryAfter: 3 * time.Minute,
223+
expectedEvictionLen: 1,
224+
expectedError: false,
225+
},
226+
{
227+
name: "task expired",
228+
binding: &workv1alpha2.ClusterResourceBinding{
229+
ObjectMeta: metav1.ObjectMeta{
230+
Name: "test-binding",
231+
},
232+
Spec: workv1alpha2.ResourceBindingSpec{
233+
Resource: workv1alpha2.ObjectReference{
234+
APIVersion: "apps/v1",
235+
Kind: "Deployment",
236+
Name: "test-deployment",
237+
},
238+
Clusters: []workv1alpha2.TargetCluster{
239+
{Name: "cluster1"},
240+
},
241+
GracefulEvictionTasks: []workv1alpha2.GracefulEvictionTask{
242+
{
243+
FromCluster: "cluster1",
244+
CreationTimestamp: &metav1.Time{Time: now.Add(-6 * time.Minute)},
245+
},
246+
},
247+
},
248+
Status: workv1alpha2.ResourceBindingStatus{
249+
AggregatedStatus: []workv1alpha2.AggregatedStatusItem{
250+
{
251+
ClusterName: "cluster1",
252+
Status: createRawExtension("Bound"),
253+
},
254+
},
255+
},
256+
},
257+
expectedRetryAfter: 0,
258+
expectedEvictionLen: 0,
259+
expectedError: false,
260+
},
261+
}
262+
263+
for _, tt := range tests {
264+
t.Run(tt.name, func(t *testing.T) {
265+
// Create a fake client with the binding object
266+
client := fake.NewClientBuilder().WithScheme(s).WithObjects(tt.binding).Build()
267+
c := &CRBGracefulEvictionController{
268+
Client: client,
269+
EventRecorder: record.NewFakeRecorder(10),
270+
GracefulEvictionTimeout: timeout,
271+
}
272+
273+
retryAfter, err := c.syncBinding(context.Background(), tt.binding)
274+
275+
if tt.expectedError {
276+
assert.Error(t, err)
277+
} else {
278+
assert.NoError(t, err)
279+
}
280+
281+
assert.True(t, almostEqual(retryAfter, tt.expectedRetryAfter, 100*time.Millisecond),
282+
"Expected retry after %v, but got %v", tt.expectedRetryAfter, retryAfter)
283+
284+
// Check the updated binding
285+
updatedBinding := &workv1alpha2.ClusterResourceBinding{}
286+
err = client.Get(context.Background(), types.NamespacedName{Name: tt.binding.Name}, updatedBinding)
287+
assert.NoError(t, err, "Failed to get updated binding")
288+
289+
actualEvictionLen := len(updatedBinding.Spec.GracefulEvictionTasks)
290+
assert.Equal(t, tt.expectedEvictionLen, actualEvictionLen,
291+
"Expected %d eviction tasks, but got %d", tt.expectedEvictionLen, actualEvictionLen)
292+
})
293+
}
294+
}
295+
296+
// Helper function to create a RawExtension from a status string
297+
func createRawExtension(status string) *runtime.RawExtension {
298+
raw, _ := json.Marshal(status)
299+
return &runtime.RawExtension{Raw: raw}
300+
}
301+
302+
// Helper function to compare two time.Duration values with a tolerance
303+
func almostEqual(a, b time.Duration, tolerance time.Duration) bool {
304+
diff := a - b
305+
return math.Abs(float64(diff)) < float64(tolerance)
306+
}

pkg/controllers/gracefuleviction/evictiontask_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -626,6 +626,21 @@ func Test_nextRetry(t *testing.T) {
626626
},
627627
want: 0,
628628
},
629+
{
630+
name: "task with custom grace period - not expired",
631+
args: args{
632+
task: []workv1alpha2.GracefulEvictionTask{
633+
{
634+
FromCluster: "member1",
635+
CreationTimestamp: &metav1.Time{Time: timeNow.Add(time.Minute * -5)},
636+
GracePeriodSeconds: ptr.To[int32](600),
637+
},
638+
},
639+
timeout: timeout,
640+
timeNow: timeNow.Time,
641+
},
642+
want: time.Minute * 5, // 10 minutes (grace period) - 5 minutes (elapsed time)
643+
},
629644
}
630645
for _, tt := range tests {
631646
t.Run(tt.name, func(t *testing.T) {

0 commit comments

Comments
 (0)