1
16
17 package endpointslicemirroring
18
19 import (
20 "context"
21 "fmt"
22 "testing"
23 "time"
24
25 v1 "k8s.io/api/core/v1"
26 discovery "k8s.io/api/discovery/v1"
27 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28 "k8s.io/apimachinery/pkg/util/wait"
29 "k8s.io/client-go/informers"
30 "k8s.io/client-go/kubernetes/fake"
31 v1core "k8s.io/client-go/kubernetes/typed/core/v1"
32 "k8s.io/client-go/tools/cache"
33 "k8s.io/client-go/tools/leaderelection/resourcelock"
34
35 "k8s.io/klog/v2"
36 "k8s.io/klog/v2/ktesting"
37 "k8s.io/kubernetes/pkg/controller"
38 )
39
40
41
42
43 var alwaysReady = func() bool { return true }
44
45 type endpointSliceMirroringController struct {
46 *Controller
47 endpointsStore cache.Store
48 endpointSliceStore cache.Store
49 serviceStore cache.Store
50 }
51
52 func newController(ctx context.Context, batchPeriod time.Duration) (*fake.Clientset, *endpointSliceMirroringController) {
53 client := newClientset()
54 informerFactory := informers.NewSharedInformerFactory(client, controller.NoResyncPeriodFunc())
55
56 esController := NewController(
57 ctx,
58 informerFactory.Core().V1().Endpoints(),
59 informerFactory.Discovery().V1().EndpointSlices(),
60 informerFactory.Core().V1().Services(),
61 int32(1000),
62 client,
63 batchPeriod)
64
65
66
67 esController.eventBroadcaster.StartLogging(klog.Infof)
68 esController.eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: client.CoreV1().Events("")})
69
70 esController.endpointsSynced = alwaysReady
71 esController.endpointSlicesSynced = alwaysReady
72 esController.servicesSynced = alwaysReady
73
74 return client, &endpointSliceMirroringController{
75 esController,
76 informerFactory.Core().V1().Endpoints().Informer().GetStore(),
77 informerFactory.Discovery().V1().EndpointSlices().Informer().GetStore(),
78 informerFactory.Core().V1().Services().Informer().GetStore(),
79 }
80 }
81
82 func TestSyncEndpoints(t *testing.T) {
83 endpointsName := "testing-sync-endpoints"
84 namespace := metav1.NamespaceDefault
85
86 testCases := []struct {
87 testName string
88 service *v1.Service
89 endpoints *v1.Endpoints
90 endpointSlices []*discovery.EndpointSlice
91 expectedNumActions int
92 expectedNumSlices int
93 }{{
94 testName: "Endpoints with no addresses",
95 service: &v1.Service{},
96 endpoints: &v1.Endpoints{
97 Subsets: []v1.EndpointSubset{{
98 Ports: []v1.EndpointPort{{Port: 80}},
99 }},
100 },
101 endpointSlices: []*discovery.EndpointSlice{},
102 expectedNumActions: 0,
103 expectedNumSlices: 0,
104 }, {
105 testName: "Endpoints with skip label true",
106 service: &v1.Service{},
107 endpoints: &v1.Endpoints{
108 ObjectMeta: metav1.ObjectMeta{
109 Labels: map[string]string{discovery.LabelSkipMirror: "true"},
110 },
111 Subsets: []v1.EndpointSubset{{
112 Ports: []v1.EndpointPort{{Port: 80}},
113 Addresses: []v1.EndpointAddress{{IP: "10.0.0.1"}},
114 }},
115 },
116 endpointSlices: []*discovery.EndpointSlice{},
117 expectedNumActions: 0,
118 expectedNumSlices: 0,
119 }, {
120 testName: "Endpoints with skip label false",
121 service: &v1.Service{},
122 endpoints: &v1.Endpoints{
123 ObjectMeta: metav1.ObjectMeta{
124 Labels: map[string]string{discovery.LabelSkipMirror: "false"},
125 },
126 Subsets: []v1.EndpointSubset{{
127 Ports: []v1.EndpointPort{{Port: 80}},
128 Addresses: []v1.EndpointAddress{{IP: "10.0.0.1"}},
129 }},
130 },
131 endpointSlices: []*discovery.EndpointSlice{},
132 expectedNumActions: 1,
133 expectedNumSlices: 1,
134 }, {
135 testName: "Endpoints with missing Service",
136 service: nil,
137 endpoints: &v1.Endpoints{
138 Subsets: []v1.EndpointSubset{{
139 Ports: []v1.EndpointPort{{Port: 80}},
140 Addresses: []v1.EndpointAddress{{IP: "10.0.0.1"}},
141 }},
142 },
143 endpointSlices: []*discovery.EndpointSlice{},
144 expectedNumActions: 0,
145 expectedNumSlices: 0,
146 }, {
147 testName: "Endpoints with Service with selector specified",
148 service: &v1.Service{
149 Spec: v1.ServiceSpec{
150 Selector: map[string]string{"foo": "bar"},
151 },
152 },
153 endpoints: &v1.Endpoints{
154 Subsets: []v1.EndpointSubset{{
155 Ports: []v1.EndpointPort{{Port: 80}},
156 Addresses: []v1.EndpointAddress{{IP: "10.0.0.1"}},
157 }},
158 },
159 endpointSlices: []*discovery.EndpointSlice{},
160 expectedNumActions: 0,
161 expectedNumSlices: 0,
162 }, {
163 testName: "Existing EndpointSlices that need to be cleaned up",
164 service: &v1.Service{},
165 endpoints: &v1.Endpoints{
166 Subsets: []v1.EndpointSubset{{
167 Ports: []v1.EndpointPort{{Port: 80}},
168 }},
169 },
170 endpointSlices: []*discovery.EndpointSlice{{
171 ObjectMeta: metav1.ObjectMeta{
172 Name: endpointsName + "-1",
173 Labels: map[string]string{
174 discovery.LabelServiceName: endpointsName,
175 discovery.LabelManagedBy: controllerName,
176 },
177 },
178 }},
179 expectedNumActions: 1,
180 expectedNumSlices: 0,
181 }, {
182 testName: "Existing EndpointSlices managed by a different controller, no addresses to sync",
183 service: &v1.Service{},
184 endpoints: &v1.Endpoints{
185 Subsets: []v1.EndpointSubset{{
186 Ports: []v1.EndpointPort{{Port: 80}},
187 }},
188 },
189 endpointSlices: []*discovery.EndpointSlice{{
190 ObjectMeta: metav1.ObjectMeta{
191 Name: endpointsName + "-1",
192 Labels: map[string]string{
193 discovery.LabelManagedBy: "something-else",
194 },
195 },
196 }},
197 expectedNumActions: 0,
198
199 expectedNumSlices: 0,
200 }, {
201 testName: "Endpoints with 1000 addresses",
202 service: &v1.Service{},
203 endpoints: &v1.Endpoints{
204 Subsets: []v1.EndpointSubset{{
205 Ports: []v1.EndpointPort{{Port: 80}},
206 Addresses: generateAddresses(1000),
207 }},
208 },
209 endpointSlices: []*discovery.EndpointSlice{},
210 expectedNumActions: 1,
211 expectedNumSlices: 1,
212 }, {
213 testName: "Endpoints with 1001 addresses - 1 should not be mirrored",
214 service: &v1.Service{},
215 endpoints: &v1.Endpoints{
216 Subsets: []v1.EndpointSubset{{
217 Ports: []v1.EndpointPort{{Port: 80}},
218 Addresses: generateAddresses(1001),
219 }},
220 },
221 endpointSlices: []*discovery.EndpointSlice{},
222 expectedNumActions: 2,
223 expectedNumSlices: 1,
224 }}
225
226 for _, tc := range testCases {
227 t.Run(tc.testName, func(t *testing.T) {
228 _, ctx := ktesting.NewTestContext(t)
229 client, esController := newController(ctx, time.Duration(0))
230 tc.endpoints.Name = endpointsName
231 tc.endpoints.Namespace = namespace
232 esController.endpointsStore.Add(tc.endpoints)
233 if tc.service != nil {
234 tc.service.Name = endpointsName
235 tc.service.Namespace = namespace
236 esController.serviceStore.Add(tc.service)
237 }
238
239 for _, epSlice := range tc.endpointSlices {
240 epSlice.Namespace = namespace
241 esController.endpointSliceStore.Add(epSlice)
242 _, err := client.DiscoveryV1().EndpointSlices(namespace).Create(context.TODO(), epSlice, metav1.CreateOptions{})
243 if err != nil {
244 t.Fatalf("Expected no error creating EndpointSlice, got %v", err)
245 }
246 }
247
248 logger, _ := ktesting.NewTestContext(t)
249 err := esController.syncEndpoints(logger, fmt.Sprintf("%s/%s", namespace, endpointsName))
250 if err != nil {
251 t.Fatalf("Unexpected error from syncEndpoints: %v", err)
252 }
253
254 numInitialActions := len(tc.endpointSlices)
255
256 err = wait.PollImmediate(time.Millisecond*100, wait.ForeverTestTimeout, func() (done bool, err error) {
257 actions := client.Actions()
258 numExtraActions := len(actions) - numInitialActions
259 if numExtraActions != tc.expectedNumActions {
260 t.Logf("Expected %d additional client actions, got %d: %#v. Will retry", tc.expectedNumActions, numExtraActions, actions[numInitialActions:])
261 return false, nil
262 }
263 return true, nil
264 })
265 if err != nil {
266 t.Fatal("Timed out waiting for expected actions")
267 }
268
269 endpointSlices := fetchEndpointSlices(t, client, namespace)
270 expectEndpointSlices(t, tc.expectedNumSlices, int(defaultMaxEndpointsPerSubset), *tc.endpoints, endpointSlices)
271 })
272 }
273 }
274
275 func TestShouldMirror(t *testing.T) {
276 testCases := []struct {
277 testName string
278 endpoints *v1.Endpoints
279 shouldMirror bool
280 }{{
281 testName: "Standard Endpoints",
282 endpoints: &v1.Endpoints{
283 ObjectMeta: metav1.ObjectMeta{
284 Name: "test-endpoints",
285 },
286 },
287 shouldMirror: true,
288 }, {
289 testName: "Endpoints with skip-mirror=true",
290 endpoints: &v1.Endpoints{
291 ObjectMeta: metav1.ObjectMeta{
292 Name: "test-endpoints",
293 Labels: map[string]string{
294 discovery.LabelSkipMirror: "true",
295 },
296 },
297 },
298 shouldMirror: false,
299 }, {
300 testName: "Endpoints with skip-mirror=invalid",
301 endpoints: &v1.Endpoints{
302 ObjectMeta: metav1.ObjectMeta{
303 Name: "test-endpoints",
304 Labels: map[string]string{
305 discovery.LabelSkipMirror: "invalid",
306 },
307 },
308 },
309 shouldMirror: true,
310 }, {
311 testName: "Endpoints with leader election annotation",
312 endpoints: &v1.Endpoints{
313 ObjectMeta: metav1.ObjectMeta{
314 Name: "test-endpoints",
315 Annotations: map[string]string{
316 resourcelock.LeaderElectionRecordAnnotationKey: "",
317 },
318 },
319 },
320 shouldMirror: false,
321 }}
322
323 for _, tc := range testCases {
324 t.Run(tc.testName, func(t *testing.T) {
325 _, ctx := ktesting.NewTestContext(t)
326 _, c := newController(ctx, time.Duration(0))
327
328 if tc.endpoints != nil {
329 err := c.endpointsStore.Add(tc.endpoints)
330 if err != nil {
331 t.Fatalf("Error adding Endpoints to store: %v", err)
332 }
333 }
334
335 shouldMirror := c.shouldMirror(tc.endpoints)
336
337 if shouldMirror != tc.shouldMirror {
338 t.Errorf("Expected %t to be returned, got %t", tc.shouldMirror, shouldMirror)
339 }
340 })
341 }
342 }
343
344 func TestEndpointSlicesMirroredForService(t *testing.T) {
345 testCases := []struct {
346 testName string
347 namespace string
348 name string
349 endpointSlice *discovery.EndpointSlice
350 expectedInList bool
351 }{{
352 testName: "Service with matching EndpointSlice",
353 namespace: "ns1",
354 name: "svc1",
355 endpointSlice: &discovery.EndpointSlice{
356 ObjectMeta: metav1.ObjectMeta{
357 Name: "example-1",
358 Namespace: "ns1",
359 Labels: map[string]string{
360 discovery.LabelServiceName: "svc1",
361 discovery.LabelManagedBy: controllerName,
362 },
363 },
364 },
365 expectedInList: true,
366 }, {
367 testName: "Service with EndpointSlice that has different namespace",
368 namespace: "ns1",
369 name: "svc1",
370 endpointSlice: &discovery.EndpointSlice{
371 ObjectMeta: metav1.ObjectMeta{
372 Name: "example-1",
373 Namespace: "ns2",
374 Labels: map[string]string{
375 discovery.LabelServiceName: "svc1",
376 discovery.LabelManagedBy: controllerName,
377 },
378 },
379 },
380 expectedInList: false,
381 }, {
382 testName: "Service with EndpointSlice that has different service name",
383 namespace: "ns1",
384 name: "svc1",
385 endpointSlice: &discovery.EndpointSlice{
386 ObjectMeta: metav1.ObjectMeta{
387 Name: "example-1",
388 Namespace: "ns1",
389 Labels: map[string]string{
390 discovery.LabelServiceName: "svc2",
391 discovery.LabelManagedBy: controllerName,
392 },
393 },
394 },
395 expectedInList: false,
396 }, {
397 testName: "Service with EndpointSlice that has different controller name",
398 namespace: "ns1",
399 name: "svc1",
400 endpointSlice: &discovery.EndpointSlice{
401 ObjectMeta: metav1.ObjectMeta{
402 Name: "example-1",
403 Namespace: "ns1",
404 Labels: map[string]string{
405 discovery.LabelServiceName: "svc1",
406 discovery.LabelManagedBy: controllerName + "foo",
407 },
408 },
409 },
410 expectedInList: false,
411 }, {
412 testName: "Service with EndpointSlice that has missing controller name",
413 namespace: "ns1",
414 name: "svc1",
415 endpointSlice: &discovery.EndpointSlice{
416 ObjectMeta: metav1.ObjectMeta{
417 Name: "example-1",
418 Namespace: "ns1",
419 Labels: map[string]string{
420 discovery.LabelServiceName: "svc1",
421 },
422 },
423 },
424 expectedInList: false,
425 }, {
426 testName: "Service with EndpointSlice that has missing service name",
427 namespace: "ns1",
428 name: "svc1",
429 endpointSlice: &discovery.EndpointSlice{
430 ObjectMeta: metav1.ObjectMeta{
431 Name: "example-1",
432 Namespace: "ns1",
433 Labels: map[string]string{
434 discovery.LabelManagedBy: controllerName,
435 },
436 },
437 },
438 expectedInList: false,
439 }}
440
441 for _, tc := range testCases {
442 t.Run(tc.testName, func(t *testing.T) {
443 _, ctx := ktesting.NewTestContext(t)
444 _, c := newController(ctx, time.Duration(0))
445
446 err := c.endpointSliceStore.Add(tc.endpointSlice)
447 if err != nil {
448 t.Fatalf("Error adding EndpointSlice to store: %v", err)
449 }
450
451 endpointSlices, err := endpointSlicesMirroredForService(c.endpointSliceLister, tc.namespace, tc.name)
452 if err != nil {
453 t.Fatalf("Expected no error, got %v", err)
454 }
455
456 if tc.expectedInList {
457 if len(endpointSlices) != 1 {
458 t.Fatalf("Expected 1 EndpointSlice to be in list, got %d", len(endpointSlices))
459 }
460
461 if endpointSlices[0].Name != tc.endpointSlice.Name {
462 t.Fatalf("Expected %s EndpointSlice to be in list, got %s", tc.endpointSlice.Name, endpointSlices[0].Name)
463 }
464 } else {
465 if len(endpointSlices) != 0 {
466 t.Fatalf("Expected no EndpointSlices to be in list, got %d", len(endpointSlices))
467 }
468 }
469 })
470 }
471 }
472
473 func generateAddresses(num int) []v1.EndpointAddress {
474 addresses := make([]v1.EndpointAddress, num)
475 for i := 0; i < num; i++ {
476 part1 := i / 255
477 part2 := i % 255
478 ip := fmt.Sprintf("10.0.%d.%d", part1, part2)
479 addresses[i] = v1.EndpointAddress{IP: ip}
480 }
481 return addresses
482 }
483
View as plain text