Skip to content

Commit f615e61

Browse files
Added tests for gracefuleviction controller
Signed-off-by: Anuj Agrawal <[email protected]>
1 parent 2e60a6e commit f615e61

File tree

3 files changed

+735
-0
lines changed

3 files changed

+735
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,379 @@
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+
"github.com/stretchr/testify/require"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
"k8s.io/apimachinery/pkg/runtime"
30+
"k8s.io/apimachinery/pkg/types"
31+
"k8s.io/client-go/tools/record"
32+
controllerruntime "sigs.k8s.io/controller-runtime"
33+
"sigs.k8s.io/controller-runtime/pkg/client"
34+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
35+
36+
workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
37+
"github.com/karmada-io/karmada/pkg/sharedcli/ratelimiterflag"
38+
)
39+
40+
func TestCRBGracefulEvictionController_Reconcile(t *testing.T) {
41+
scheme := runtime.NewScheme()
42+
err := workv1alpha2.Install(scheme)
43+
assert.NoError(t, err, "Failed to add workv1alpha2 to scheme")
44+
now := metav1.Now()
45+
testCases := []struct {
46+
name string
47+
binding *workv1alpha2.ClusterResourceBinding
48+
expectedResult controllerruntime.Result
49+
expectedError bool
50+
expectedRequeue bool
51+
notFound bool
52+
}{
53+
{
54+
name: "binding with no graceful eviction tasks",
55+
binding: &workv1alpha2.ClusterResourceBinding{
56+
ObjectMeta: metav1.ObjectMeta{
57+
Name: "test-binding",
58+
},
59+
Spec: workv1alpha2.ResourceBindingSpec{
60+
GracefulEvictionTasks: []workv1alpha2.GracefulEvictionTask{},
61+
},
62+
},
63+
expectedResult: controllerruntime.Result{},
64+
expectedError: false,
65+
expectedRequeue: false,
66+
},
67+
{
68+
name: "binding with active graceful eviction tasks",
69+
binding: &workv1alpha2.ClusterResourceBinding{
70+
ObjectMeta: metav1.ObjectMeta{
71+
Name: "test-binding",
72+
},
73+
Spec: workv1alpha2.ResourceBindingSpec{
74+
GracefulEvictionTasks: []workv1alpha2.GracefulEvictionTask{
75+
{
76+
FromCluster: "cluster1",
77+
CreationTimestamp: &now,
78+
},
79+
},
80+
},
81+
Status: workv1alpha2.ResourceBindingStatus{
82+
SchedulerObservedGeneration: 1,
83+
},
84+
},
85+
expectedResult: controllerruntime.Result{},
86+
expectedError: false,
87+
expectedRequeue: false,
88+
},
89+
{
90+
name: "binding marked for deletion",
91+
binding: &workv1alpha2.ClusterResourceBinding{
92+
ObjectMeta: metav1.ObjectMeta{
93+
Name: "test-binding",
94+
DeletionTimestamp: &now,
95+
Finalizers: []string{"test-finalizer"},
96+
},
97+
},
98+
expectedResult: controllerruntime.Result{},
99+
expectedError: false,
100+
expectedRequeue: false,
101+
},
102+
{
103+
name: "binding not found",
104+
binding: &workv1alpha2.ClusterResourceBinding{
105+
ObjectMeta: metav1.ObjectMeta{
106+
Name: "non-existent-binding",
107+
},
108+
},
109+
expectedResult: controllerruntime.Result{},
110+
expectedError: false,
111+
expectedRequeue: false,
112+
notFound: true,
113+
},
114+
}
115+
for _, tc := range testCases {
116+
t.Run(tc.name, func(t *testing.T) {
117+
// Create a fake client with or without the binding object
118+
var client client.Client
119+
if tc.notFound {
120+
client = fake.NewClientBuilder().WithScheme(scheme).Build()
121+
} else {
122+
client = fake.NewClientBuilder().WithScheme(scheme).WithObjects(tc.binding).Build()
123+
}
124+
c := &CRBGracefulEvictionController{
125+
Client: client,
126+
EventRecorder: record.NewFakeRecorder(10),
127+
RateLimiterOptions: ratelimiterflag.Options{},
128+
GracefulEvictionTimeout: 5 * time.Minute,
129+
}
130+
result, err := c.Reconcile(context.TODO(), controllerruntime.Request{
131+
NamespacedName: types.NamespacedName{
132+
Name: tc.binding.Name,
133+
},
134+
})
135+
if tc.expectedError {
136+
assert.Error(t, err)
137+
} else {
138+
assert.NoError(t, err)
139+
}
140+
assert.Equal(t, tc.expectedResult, result)
141+
if tc.expectedRequeue {
142+
assert.True(t, result.RequeueAfter > 0, "Expected requeue, but got no requeue")
143+
} else {
144+
assert.Zero(t, result.RequeueAfter, "Expected no requeue, but got requeue")
145+
}
146+
// Verify the binding was updated, unless it's the "not found" case
147+
if !tc.notFound {
148+
updatedBinding := &workv1alpha2.ClusterResourceBinding{}
149+
err = client.Get(context.TODO(), types.NamespacedName{Name: tc.binding.Name}, updatedBinding)
150+
assert.NoError(t, err)
151+
}
152+
})
153+
}
154+
}
155+
156+
func TestCRBGracefulEvictionController_syncBinding(t *testing.T) {
157+
now := metav1.Now()
158+
timeout := 5 * time.Minute
159+
160+
s := runtime.NewScheme()
161+
err := workv1alpha2.Install(s)
162+
assert.NoError(t, err, "Failed to add workv1alpha2 to scheme")
163+
164+
tests := []struct {
165+
name string
166+
binding *workv1alpha2.ClusterResourceBinding
167+
expectedRetryAfter time.Duration
168+
expectedEvictionLen int
169+
expectedError bool
170+
}{
171+
{
172+
name: "no tasks",
173+
binding: &workv1alpha2.ClusterResourceBinding{
174+
ObjectMeta: metav1.ObjectMeta{
175+
Name: "test-binding",
176+
},
177+
Spec: workv1alpha2.ResourceBindingSpec{
178+
Resource: workv1alpha2.ObjectReference{
179+
APIVersion: "apps/v1",
180+
Kind: "Deployment",
181+
Name: "test-deployment",
182+
},
183+
Clusters: []workv1alpha2.TargetCluster{
184+
{Name: "cluster1"},
185+
},
186+
},
187+
},
188+
expectedRetryAfter: 0,
189+
expectedEvictionLen: 0,
190+
expectedError: false,
191+
},
192+
{
193+
name: "task not expired",
194+
binding: &workv1alpha2.ClusterResourceBinding{
195+
ObjectMeta: metav1.ObjectMeta{
196+
Name: "test-binding",
197+
},
198+
Spec: workv1alpha2.ResourceBindingSpec{
199+
Resource: workv1alpha2.ObjectReference{
200+
APIVersion: "apps/v1",
201+
Kind: "Deployment",
202+
Name: "test-deployment",
203+
},
204+
Clusters: []workv1alpha2.TargetCluster{
205+
{Name: "cluster1"},
206+
},
207+
GracefulEvictionTasks: []workv1alpha2.GracefulEvictionTask{
208+
{
209+
FromCluster: "cluster1",
210+
CreationTimestamp: &metav1.Time{Time: now.Add(-2 * time.Minute)},
211+
},
212+
},
213+
},
214+
Status: workv1alpha2.ResourceBindingStatus{
215+
AggregatedStatus: []workv1alpha2.AggregatedStatusItem{
216+
{
217+
ClusterName: "cluster1",
218+
Status: createRawExtension("Bound"),
219+
},
220+
},
221+
},
222+
},
223+
expectedRetryAfter: 3 * time.Minute,
224+
expectedEvictionLen: 1,
225+
expectedError: false,
226+
},
227+
{
228+
name: "task expired",
229+
binding: &workv1alpha2.ClusterResourceBinding{
230+
ObjectMeta: metav1.ObjectMeta{
231+
Name: "test-binding",
232+
},
233+
Spec: workv1alpha2.ResourceBindingSpec{
234+
Resource: workv1alpha2.ObjectReference{
235+
APIVersion: "apps/v1",
236+
Kind: "Deployment",
237+
Name: "test-deployment",
238+
},
239+
Clusters: []workv1alpha2.TargetCluster{
240+
{Name: "cluster1"},
241+
},
242+
GracefulEvictionTasks: []workv1alpha2.GracefulEvictionTask{
243+
{
244+
FromCluster: "cluster1",
245+
CreationTimestamp: &metav1.Time{Time: now.Add(-6 * time.Minute)},
246+
},
247+
},
248+
},
249+
Status: workv1alpha2.ResourceBindingStatus{
250+
AggregatedStatus: []workv1alpha2.AggregatedStatusItem{
251+
{
252+
ClusterName: "cluster1",
253+
Status: createRawExtension("Bound"),
254+
},
255+
},
256+
},
257+
},
258+
expectedRetryAfter: 0,
259+
expectedEvictionLen: 0,
260+
expectedError: false,
261+
},
262+
}
263+
264+
for _, tt := range tests {
265+
t.Run(tt.name, func(t *testing.T) {
266+
// Create a fake client with the binding object
267+
client := fake.NewClientBuilder().WithScheme(s).WithObjects(tt.binding).Build()
268+
c := &CRBGracefulEvictionController{
269+
Client: client,
270+
EventRecorder: record.NewFakeRecorder(10),
271+
GracefulEvictionTimeout: timeout,
272+
}
273+
274+
retryAfter, err := c.syncBinding(context.Background(), tt.binding)
275+
276+
if tt.expectedError {
277+
assert.Error(t, err)
278+
} else {
279+
assert.NoError(t, err)
280+
}
281+
282+
assert.True(t, almostEqual(retryAfter, tt.expectedRetryAfter, 100*time.Millisecond),
283+
"Expected retry after %v, but got %v", tt.expectedRetryAfter, retryAfter)
284+
285+
// Check the updated binding
286+
updatedBinding := &workv1alpha2.ClusterResourceBinding{}
287+
err = client.Get(context.Background(), types.NamespacedName{Name: tt.binding.Name}, updatedBinding)
288+
assert.NoError(t, err, "Failed to get updated binding")
289+
290+
actualEvictionLen := len(updatedBinding.Spec.GracefulEvictionTasks)
291+
assert.Equal(t, tt.expectedEvictionLen, actualEvictionLen,
292+
"Expected %d eviction tasks, but got %d", tt.expectedEvictionLen, actualEvictionLen)
293+
})
294+
}
295+
}
296+
297+
// Helper function to create a RawExtension from a status string
298+
func createRawExtension(status string) *runtime.RawExtension {
299+
raw, _ := json.Marshal(status)
300+
return &runtime.RawExtension{Raw: raw}
301+
}
302+
303+
// Helper function to compare two time.Duration values with a tolerance
304+
func almostEqual(a, b time.Duration, tolerance time.Duration) bool {
305+
diff := a - b
306+
return math.Abs(float64(diff)) < float64(tolerance)
307+
}
308+
309+
func TestCRBGracefulEvictionController_SetupWithManager(t *testing.T) {
310+
scheme := runtime.NewScheme()
311+
err := workv1alpha2.Install(scheme)
312+
require.NoError(t, err, "Failed to add workv1alpha2 to scheme")
313+
314+
tests := []struct {
315+
name string
316+
c *CRBGracefulEvictionController
317+
wantErr bool
318+
}{
319+
{
320+
name: "successful setup",
321+
c: &CRBGracefulEvictionController{
322+
Client: fake.NewClientBuilder().WithScheme(scheme).Build(),
323+
EventRecorder: record.NewFakeRecorder(100),
324+
RateLimiterOptions: ratelimiterflag.Options{
325+
RateLimiterBaseDelay: 1 * time.Millisecond,
326+
RateLimiterMaxDelay: 1000 * time.Millisecond,
327+
RateLimiterQPS: 10,
328+
RateLimiterBucketSize: 100,
329+
},
330+
GracefulEvictionTimeout: 5 * time.Minute,
331+
},
332+
wantErr: false,
333+
},
334+
{
335+
name: "setup with invalid rate limiter options",
336+
c: &CRBGracefulEvictionController{
337+
Client: fake.NewClientBuilder().WithScheme(scheme).Build(),
338+
EventRecorder: record.NewFakeRecorder(100),
339+
RateLimiterOptions: ratelimiterflag.Options{
340+
RateLimiterBaseDelay: -1 * time.Millisecond, // Invalid value
341+
RateLimiterMaxDelay: 1000 * time.Millisecond,
342+
RateLimiterQPS: 10,
343+
RateLimiterBucketSize: 100,
344+
},
345+
GracefulEvictionTimeout: 5 * time.Minute,
346+
},
347+
wantErr: false,
348+
},
349+
{
350+
name: "setup with different rate limiter options",
351+
c: &CRBGracefulEvictionController{
352+
Client: fake.NewClientBuilder().WithScheme(scheme).Build(),
353+
EventRecorder: record.NewFakeRecorder(100),
354+
RateLimiterOptions: ratelimiterflag.Options{
355+
RateLimiterBaseDelay: 5 * time.Millisecond,
356+
RateLimiterMaxDelay: 2000 * time.Millisecond,
357+
RateLimiterQPS: 5,
358+
RateLimiterBucketSize: 50,
359+
},
360+
GracefulEvictionTimeout: 10 * time.Minute,
361+
},
362+
wantErr: false,
363+
},
364+
}
365+
366+
for _, tt := range tests {
367+
t.Run(tt.name, func(t *testing.T) {
368+
mgr, err := controllerruntime.NewManager(controllerruntime.GetConfigOrDie(), controllerruntime.Options{Scheme: scheme})
369+
require.NoError(t, err, "Failed to create manager")
370+
371+
err = tt.c.SetupWithManager(mgr)
372+
if tt.wantErr {
373+
assert.Error(t, err, "SetupWithManager() should have returned an error")
374+
} else {
375+
assert.NoError(t, err, "SetupWithManager() should not have returned an error")
376+
}
377+
})
378+
}
379+
}

0 commit comments

Comments
 (0)