k8s.io/kubernetes@v1.29.3/pkg/controller/endpointslicemirroring/endpointslicemirroring_controller_test.go (about) 1 /* 2 Copyright 2020 The Kubernetes 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 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 // Most of the tests related to EndpointSlice allocation can be found in reconciler_test.go 41 // Tests here primarily focus on unique controller functionality before the reconciler begins 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 // The event processing pipeline is normally started via Run() method. 66 // However, since we don't start it in unit tests, we explicitly start it here. 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 // This only queries for EndpointSlices managed by this controller. 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, // extra action for creating warning event 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 // Wait for the expected event show up in test "Endpoints with 1001 addresses - 1 should not be mirrored" 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 }