k8s.io/kubernetes@v1.29.3/pkg/controller/endpointslicemirroring/reconciler_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 "strings" 22 "testing" 23 24 corev1 "k8s.io/api/core/v1" 25 discovery "k8s.io/api/discovery/v1" 26 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 27 "k8s.io/client-go/kubernetes/fake" 28 "k8s.io/client-go/kubernetes/scheme" 29 "k8s.io/client-go/tools/record" 30 "k8s.io/component-base/metrics/testutil" 31 endpointsliceutil "k8s.io/endpointslice/util" 32 "k8s.io/klog/v2/ktesting" 33 endpointsv1 "k8s.io/kubernetes/pkg/api/v1/endpoints" 34 "k8s.io/kubernetes/pkg/controller/endpointslicemirroring/metrics" 35 "k8s.io/utils/pointer" 36 ) 37 38 const defaultMaxEndpointsPerSubset = int32(1000) 39 40 // TestReconcile ensures that Endpoints are reconciled into corresponding 41 // EndpointSlices with appropriate fields. 42 func TestReconcile(t *testing.T) { 43 protoTCP := corev1.ProtocolTCP 44 protoUDP := corev1.ProtocolUDP 45 46 testCases := []struct { 47 testName string 48 subsets []corev1.EndpointSubset 49 epLabels map[string]string 50 epAnnotations map[string]string 51 endpointsDeletionPending bool 52 maxEndpointsPerSubset int32 53 existingEndpointSlices []*discovery.EndpointSlice 54 expectedNumSlices int 55 expectedClientActions int 56 expectedMetrics *expectedMetrics 57 }{{ 58 testName: "Endpoints with no subsets", 59 subsets: []corev1.EndpointSubset{}, 60 existingEndpointSlices: []*discovery.EndpointSlice{}, 61 expectedNumSlices: 0, 62 expectedClientActions: 0, 63 expectedMetrics: &expectedMetrics{}, 64 }, { 65 testName: "Endpoints with no addresses", 66 subsets: []corev1.EndpointSubset{{ 67 Ports: []corev1.EndpointPort{{ 68 Name: "http", 69 Port: 80, 70 Protocol: corev1.ProtocolTCP, 71 }}, 72 }}, 73 existingEndpointSlices: []*discovery.EndpointSlice{}, 74 expectedNumSlices: 0, 75 expectedClientActions: 0, 76 expectedMetrics: &expectedMetrics{}, 77 }, { 78 testName: "Endpoints with 1 subset, port, and address", 79 subsets: []corev1.EndpointSubset{{ 80 Ports: []corev1.EndpointPort{{ 81 Name: "http", 82 Port: 80, 83 Protocol: corev1.ProtocolTCP, 84 }}, 85 Addresses: []corev1.EndpointAddress{{ 86 IP: "10.0.0.1", 87 Hostname: "pod-1", 88 NodeName: pointer.String("node-1"), 89 }}, 90 }}, 91 existingEndpointSlices: []*discovery.EndpointSlice{}, 92 expectedNumSlices: 1, 93 expectedClientActions: 1, 94 expectedMetrics: &expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 1, addedPerSync: 1, numCreated: 1}, 95 }, { 96 testName: "Endpoints with 2 subset, different port and address", 97 subsets: []corev1.EndpointSubset{ 98 { 99 Ports: []corev1.EndpointPort{{ 100 Name: "http", 101 Port: 80, 102 Protocol: corev1.ProtocolTCP, 103 }}, 104 Addresses: []corev1.EndpointAddress{{ 105 IP: "10.0.0.1", 106 Hostname: "pod-1", 107 NodeName: pointer.String("node-1"), 108 }}, 109 }, 110 { 111 Ports: []corev1.EndpointPort{{ 112 Name: "https", 113 Port: 443, 114 Protocol: corev1.ProtocolTCP, 115 }}, 116 Addresses: []corev1.EndpointAddress{{ 117 IP: "10.0.0.2", 118 Hostname: "pod-2", 119 NodeName: pointer.String("node-1"), 120 }}, 121 }, 122 }, 123 existingEndpointSlices: []*discovery.EndpointSlice{}, 124 expectedNumSlices: 2, 125 expectedClientActions: 2, 126 expectedMetrics: &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 2, addedPerSync: 2, numCreated: 2}, 127 }, { 128 testName: "Endpoints with 2 subset, different port and same address", 129 subsets: []corev1.EndpointSubset{ 130 { 131 Ports: []corev1.EndpointPort{{ 132 Name: "http", 133 Port: 80, 134 Protocol: corev1.ProtocolTCP, 135 }}, 136 Addresses: []corev1.EndpointAddress{{ 137 IP: "10.0.0.1", 138 Hostname: "pod-1", 139 NodeName: pointer.String("node-1"), 140 }}, 141 }, 142 { 143 Ports: []corev1.EndpointPort{{ 144 Name: "https", 145 Port: 443, 146 Protocol: corev1.ProtocolTCP, 147 }}, 148 Addresses: []corev1.EndpointAddress{{ 149 IP: "10.0.0.1", 150 Hostname: "pod-1", 151 NodeName: pointer.String("node-1"), 152 }}, 153 }, 154 }, 155 existingEndpointSlices: []*discovery.EndpointSlice{}, 156 expectedNumSlices: 1, 157 expectedClientActions: 1, 158 expectedMetrics: &expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 1, addedPerSync: 1, numCreated: 1}, 159 }, { 160 testName: "Endpoints with 2 subset, different address and same port", 161 subsets: []corev1.EndpointSubset{ 162 { 163 Ports: []corev1.EndpointPort{{ 164 Name: "http", 165 Port: 80, 166 Protocol: corev1.ProtocolTCP, 167 }}, 168 Addresses: []corev1.EndpointAddress{{ 169 IP: "10.0.0.1", 170 Hostname: "pod-1", 171 NodeName: pointer.String("node-1"), 172 }}, 173 }, 174 { 175 Ports: []corev1.EndpointPort{{ 176 Name: "http", 177 Port: 80, 178 Protocol: corev1.ProtocolTCP, 179 }}, 180 Addresses: []corev1.EndpointAddress{{ 181 IP: "10.0.0.2", 182 Hostname: "pod-2", 183 NodeName: pointer.String("node-1"), 184 }}, 185 }, 186 }, 187 existingEndpointSlices: []*discovery.EndpointSlice{}, 188 expectedNumSlices: 1, 189 expectedClientActions: 1, 190 expectedMetrics: &expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 2, addedPerSync: 2, numCreated: 1}, 191 }, { 192 testName: "Endpoints with 1 subset, port, and address, pending deletion", 193 subsets: []corev1.EndpointSubset{{ 194 Ports: []corev1.EndpointPort{{ 195 Name: "http", 196 Port: 80, 197 Protocol: corev1.ProtocolTCP, 198 }}, 199 Addresses: []corev1.EndpointAddress{{ 200 IP: "10.0.0.1", 201 Hostname: "pod-1", 202 NodeName: pointer.String("node-1"), 203 }}, 204 }}, 205 endpointsDeletionPending: true, 206 existingEndpointSlices: []*discovery.EndpointSlice{}, 207 expectedNumSlices: 0, 208 expectedClientActions: 0, 209 }, { 210 testName: "Endpoints with 1 subset, port, and address and existing slice with same fields", 211 subsets: []corev1.EndpointSubset{{ 212 Ports: []corev1.EndpointPort{{ 213 Name: "http", 214 Port: 80, 215 Protocol: corev1.ProtocolTCP, 216 }}, 217 Addresses: []corev1.EndpointAddress{{ 218 IP: "10.0.0.1", 219 Hostname: "pod-1", 220 }}, 221 }}, 222 existingEndpointSlices: []*discovery.EndpointSlice{{ 223 ObjectMeta: metav1.ObjectMeta{ 224 Name: "test-ep-1", 225 }, 226 AddressType: discovery.AddressTypeIPv4, 227 Ports: []discovery.EndpointPort{{ 228 Name: pointer.String("http"), 229 Port: pointer.Int32(80), 230 Protocol: &protoTCP, 231 }}, 232 Endpoints: []discovery.Endpoint{{ 233 Addresses: []string{"10.0.0.1"}, 234 Hostname: pointer.String("pod-1"), 235 Conditions: discovery.EndpointConditions{Ready: pointer.Bool(true)}, 236 }}, 237 }}, 238 expectedNumSlices: 1, 239 expectedClientActions: 0, 240 }, { 241 testName: "Endpoints with 1 subset, port, and address and existing slice with an additional annotation", 242 subsets: []corev1.EndpointSubset{{ 243 Ports: []corev1.EndpointPort{{ 244 Name: "http", 245 Port: 80, 246 Protocol: corev1.ProtocolTCP, 247 }}, 248 Addresses: []corev1.EndpointAddress{{ 249 IP: "10.0.0.1", 250 Hostname: "pod-1", 251 }}, 252 }}, 253 existingEndpointSlices: []*discovery.EndpointSlice{{ 254 ObjectMeta: metav1.ObjectMeta{ 255 Name: "test-ep-1", 256 Annotations: map[string]string{"foo": "bar"}, 257 }, 258 AddressType: discovery.AddressTypeIPv4, 259 Ports: []discovery.EndpointPort{{ 260 Name: pointer.String("http"), 261 Port: pointer.Int32(80), 262 Protocol: &protoTCP, 263 }}, 264 Endpoints: []discovery.Endpoint{{ 265 Addresses: []string{"10.0.0.1"}, 266 Hostname: pointer.String("pod-1"), 267 Conditions: discovery.EndpointConditions{Ready: pointer.Bool(true)}, 268 }}, 269 }}, 270 expectedNumSlices: 1, 271 expectedClientActions: 1, 272 }, { 273 testName: "Endpoints with 1 subset, port, label and address and existing slice with same fields but the label", 274 subsets: []corev1.EndpointSubset{{ 275 Ports: []corev1.EndpointPort{{ 276 Name: "http", 277 Port: 80, 278 Protocol: corev1.ProtocolTCP, 279 }}, 280 Addresses: []corev1.EndpointAddress{{ 281 IP: "10.0.0.1", 282 Hostname: "pod-1", 283 }}, 284 }}, 285 epLabels: map[string]string{"foo": "bar"}, 286 existingEndpointSlices: []*discovery.EndpointSlice{{ 287 ObjectMeta: metav1.ObjectMeta{ 288 Name: "test-ep-1", 289 Annotations: map[string]string{"foo": "bar"}, 290 }, 291 AddressType: discovery.AddressTypeIPv4, 292 Ports: []discovery.EndpointPort{{ 293 Name: pointer.String("http"), 294 Port: pointer.Int32(80), 295 Protocol: &protoTCP, 296 }}, 297 Endpoints: []discovery.Endpoint{{ 298 Addresses: []string{"10.0.0.1"}, 299 Hostname: pointer.String("pod-1"), 300 Conditions: discovery.EndpointConditions{Ready: pointer.Bool(true)}, 301 }}, 302 }}, 303 expectedNumSlices: 1, 304 expectedClientActions: 1, 305 }, { 306 testName: "Endpoints with 1 subset, 2 ports, and 2 addresses", 307 subsets: []corev1.EndpointSubset{{ 308 Ports: []corev1.EndpointPort{{ 309 Name: "http", 310 Port: 80, 311 Protocol: corev1.ProtocolTCP, 312 }, { 313 Name: "https", 314 Port: 443, 315 Protocol: corev1.ProtocolUDP, 316 }}, 317 Addresses: []corev1.EndpointAddress{{ 318 IP: "10.0.0.1", 319 Hostname: "pod-1", 320 NodeName: pointer.String("node-1"), 321 }, { 322 IP: "10.0.0.2", 323 Hostname: "pod-2", 324 NodeName: pointer.String("node-2"), 325 }}, 326 }}, 327 existingEndpointSlices: []*discovery.EndpointSlice{}, 328 expectedNumSlices: 1, 329 expectedClientActions: 1, 330 expectedMetrics: &expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 2, addedPerSync: 2, numCreated: 1}, 331 }, { 332 testName: "Endpoints with 1 subset, 2 ports, and 2 not ready addresses", 333 subsets: []corev1.EndpointSubset{{ 334 Ports: []corev1.EndpointPort{{ 335 Name: "http", 336 Port: 80, 337 Protocol: corev1.ProtocolTCP, 338 }, { 339 Name: "https", 340 Port: 443, 341 Protocol: corev1.ProtocolUDP, 342 }}, 343 NotReadyAddresses: []corev1.EndpointAddress{{ 344 IP: "10.0.0.1", 345 Hostname: "pod-1", 346 NodeName: pointer.String("node-1"), 347 }, { 348 IP: "10.0.0.2", 349 Hostname: "pod-2", 350 NodeName: pointer.String("node-2"), 351 }}, 352 }}, 353 existingEndpointSlices: []*discovery.EndpointSlice{}, 354 expectedNumSlices: 1, 355 expectedClientActions: 1, 356 expectedMetrics: &expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 2, addedPerSync: 2, numCreated: 1}, 357 }, { 358 testName: "Endpoints with 1 subset, 2 ports, and 2 ready and 2 not ready addresses", 359 subsets: []corev1.EndpointSubset{{ 360 Ports: []corev1.EndpointPort{{ 361 Name: "http", 362 Port: 80, 363 Protocol: corev1.ProtocolTCP, 364 }, { 365 Name: "https", 366 Port: 443, 367 Protocol: corev1.ProtocolUDP, 368 }}, 369 Addresses: []corev1.EndpointAddress{{ 370 IP: "10.1.1.1", 371 Hostname: "pod-11", 372 NodeName: pointer.String("node-1"), 373 }, { 374 IP: "10.1.1.2", 375 Hostname: "pod-12", 376 NodeName: pointer.String("node-2"), 377 }}, 378 NotReadyAddresses: []corev1.EndpointAddress{{ 379 IP: "10.0.0.1", 380 Hostname: "pod-1", 381 NodeName: pointer.String("node-1"), 382 }, { 383 IP: "10.0.0.2", 384 Hostname: "pod-2", 385 NodeName: pointer.String("node-2"), 386 }}, 387 }}, 388 existingEndpointSlices: []*discovery.EndpointSlice{}, 389 expectedNumSlices: 1, 390 expectedClientActions: 1, 391 expectedMetrics: &expectedMetrics{desiredSlices: 1, actualSlices: 1, desiredEndpoints: 4, addedPerSync: 4, numCreated: 1}, 392 }, { 393 testName: "Endpoints with 2 subsets, multiple ports and addresses", 394 subsets: []corev1.EndpointSubset{{ 395 Ports: []corev1.EndpointPort{{ 396 Name: "http", 397 Port: 80, 398 Protocol: corev1.ProtocolTCP, 399 }, { 400 Name: "https", 401 Port: 443, 402 Protocol: corev1.ProtocolUDP, 403 }}, 404 Addresses: []corev1.EndpointAddress{{ 405 IP: "10.0.0.1", 406 Hostname: "pod-1", 407 NodeName: pointer.String("node-1"), 408 }, { 409 IP: "10.0.0.2", 410 Hostname: "pod-2", 411 NodeName: pointer.String("node-2"), 412 }}, 413 }, { 414 Ports: []corev1.EndpointPort{{ 415 Name: "http", 416 Port: 3000, 417 Protocol: corev1.ProtocolTCP, 418 }, { 419 Name: "https", 420 Port: 3001, 421 Protocol: corev1.ProtocolUDP, 422 }}, 423 Addresses: []corev1.EndpointAddress{{ 424 IP: "10.0.1.1", 425 Hostname: "pod-11", 426 NodeName: pointer.String("node-1"), 427 }, { 428 IP: "10.0.1.2", 429 Hostname: "pod-12", 430 NodeName: pointer.String("node-2"), 431 }, { 432 IP: "10.0.1.3", 433 Hostname: "pod-13", 434 NodeName: pointer.String("node-3"), 435 }}, 436 }}, 437 existingEndpointSlices: []*discovery.EndpointSlice{}, 438 expectedNumSlices: 2, 439 expectedClientActions: 2, 440 expectedMetrics: &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 5, numCreated: 2}, 441 }, { 442 testName: "Endpoints with 2 subsets, multiple ports and addresses, existing empty EndpointSlice", 443 subsets: []corev1.EndpointSubset{{ 444 Ports: []corev1.EndpointPort{{ 445 Name: "http", 446 Port: 80, 447 Protocol: corev1.ProtocolTCP, 448 }, { 449 Name: "https", 450 Port: 443, 451 Protocol: corev1.ProtocolUDP, 452 }}, 453 Addresses: []corev1.EndpointAddress{{ 454 IP: "10.0.0.1", 455 Hostname: "pod-1", 456 NodeName: pointer.String("node-1"), 457 }, { 458 IP: "10.0.0.2", 459 Hostname: "pod-2", 460 NodeName: pointer.String("node-2"), 461 }}, 462 }, { 463 Ports: []corev1.EndpointPort{{ 464 Name: "http", 465 Port: 3000, 466 Protocol: corev1.ProtocolTCP, 467 }, { 468 Name: "https", 469 Port: 3001, 470 Protocol: corev1.ProtocolUDP, 471 }}, 472 Addresses: []corev1.EndpointAddress{{ 473 IP: "10.0.1.1", 474 Hostname: "pod-11", 475 NodeName: pointer.String("node-1"), 476 }, { 477 IP: "10.0.1.2", 478 Hostname: "pod-12", 479 NodeName: pointer.String("node-2"), 480 }, { 481 IP: "10.0.1.3", 482 Hostname: "pod-13", 483 NodeName: pointer.String("node-3"), 484 }}, 485 }}, 486 existingEndpointSlices: []*discovery.EndpointSlice{{ 487 ObjectMeta: metav1.ObjectMeta{ 488 Name: "test-ep-1", 489 }, 490 AddressType: discovery.AddressTypeIPv4, 491 Ports: []discovery.EndpointPort{{ 492 Name: pointer.String("http"), 493 Port: pointer.Int32(80), 494 Protocol: &protoTCP, 495 }, { 496 Name: pointer.String("https"), 497 Port: pointer.Int32(443), 498 Protocol: &protoUDP, 499 }}, 500 }}, 501 expectedNumSlices: 2, 502 expectedClientActions: 2, 503 expectedMetrics: &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 5, numCreated: 1, numUpdated: 1}, 504 }, { 505 testName: "Endpoints with 2 subsets, multiple ports and addresses, existing EndpointSlice with some addresses", 506 subsets: []corev1.EndpointSubset{{ 507 Ports: []corev1.EndpointPort{{ 508 Name: "http", 509 Port: 80, 510 Protocol: corev1.ProtocolTCP, 511 }, { 512 Name: "https", 513 Port: 443, 514 Protocol: corev1.ProtocolUDP, 515 }}, 516 Addresses: []corev1.EndpointAddress{{ 517 IP: "10.0.0.1", 518 Hostname: "pod-1", 519 NodeName: pointer.String("node-1"), 520 }, { 521 IP: "10.0.0.2", 522 Hostname: "pod-2", 523 NodeName: pointer.String("node-2"), 524 }}, 525 }, { 526 Ports: []corev1.EndpointPort{{ 527 Name: "http", 528 Port: 3000, 529 Protocol: corev1.ProtocolTCP, 530 }, { 531 Name: "https", 532 Port: 3001, 533 Protocol: corev1.ProtocolUDP, 534 }}, 535 Addresses: []corev1.EndpointAddress{{ 536 IP: "10.0.1.1", 537 Hostname: "pod-11", 538 NodeName: pointer.String("node-1"), 539 }, { 540 IP: "10.0.1.2", 541 Hostname: "pod-12", 542 NodeName: pointer.String("node-2"), 543 }, { 544 IP: "10.0.1.3", 545 Hostname: "pod-13", 546 NodeName: pointer.String("node-3"), 547 }}, 548 }}, 549 existingEndpointSlices: []*discovery.EndpointSlice{{ 550 ObjectMeta: metav1.ObjectMeta{ 551 Name: "test-ep-1", 552 }, 553 AddressType: discovery.AddressTypeIPv4, 554 Ports: []discovery.EndpointPort{{ 555 Name: pointer.String("http"), 556 Port: pointer.Int32(80), 557 Protocol: &protoTCP, 558 }, { 559 Name: pointer.String("https"), 560 Port: pointer.Int32(443), 561 Protocol: &protoUDP, 562 }}, 563 Endpoints: []discovery.Endpoint{{ 564 Addresses: []string{"10.0.0.2"}, 565 Hostname: pointer.String("pod-2"), 566 }, { 567 Addresses: []string{"10.0.0.1", "10.0.0.3"}, 568 Hostname: pointer.String("pod-1"), 569 }}, 570 }}, 571 expectedNumSlices: 2, 572 expectedClientActions: 2, 573 expectedMetrics: &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 4, updatedPerSync: 1, removedPerSync: 1, numCreated: 1, numUpdated: 1}, 574 }, { 575 testName: "Endpoints with 2 subsets, multiple ports and addresses, existing EndpointSlice identical to subset", 576 subsets: []corev1.EndpointSubset{{ 577 Ports: []corev1.EndpointPort{{ 578 Name: "http", 579 Port: 80, 580 Protocol: corev1.ProtocolTCP, 581 }, { 582 Name: "https", 583 Port: 443, 584 Protocol: corev1.ProtocolUDP, 585 }}, 586 Addresses: []corev1.EndpointAddress{{ 587 IP: "10.0.0.1", 588 Hostname: "pod-1", 589 NodeName: pointer.String("node-1"), 590 }, { 591 IP: "10.0.0.2", 592 Hostname: "pod-2", 593 NodeName: pointer.String("node-2"), 594 }}, 595 }, { 596 Ports: []corev1.EndpointPort{{ 597 Name: "http", 598 Port: 3000, 599 Protocol: corev1.ProtocolTCP, 600 }, { 601 Name: "https", 602 Port: 3001, 603 Protocol: corev1.ProtocolUDP, 604 }}, 605 Addresses: []corev1.EndpointAddress{{ 606 IP: "10.0.1.1", 607 Hostname: "pod-11", 608 NodeName: pointer.String("node-1"), 609 }, { 610 IP: "10.0.1.2", 611 Hostname: "pod-12", 612 NodeName: pointer.String("node-2"), 613 }, { 614 IP: "10.0.1.3", 615 Hostname: "pod-13", 616 NodeName: pointer.String("node-3"), 617 }}, 618 }}, 619 existingEndpointSlices: []*discovery.EndpointSlice{{ 620 ObjectMeta: metav1.ObjectMeta{ 621 Name: "test-ep-1", 622 }, 623 AddressType: discovery.AddressTypeIPv4, 624 Ports: []discovery.EndpointPort{{ 625 Name: pointer.String("http"), 626 Port: pointer.Int32(80), 627 Protocol: &protoTCP, 628 }, { 629 Name: pointer.String("https"), 630 Port: pointer.Int32(443), 631 Protocol: &protoUDP, 632 }}, 633 Endpoints: []discovery.Endpoint{{ 634 Addresses: []string{"10.0.0.1"}, 635 Hostname: pointer.String("pod-1"), 636 NodeName: pointer.String("node-1"), 637 Conditions: discovery.EndpointConditions{Ready: pointer.Bool(true)}, 638 }, { 639 Addresses: []string{"10.0.0.2"}, 640 Hostname: pointer.String("pod-2"), 641 NodeName: pointer.String("node-2"), 642 Conditions: discovery.EndpointConditions{Ready: pointer.Bool(true)}, 643 }}, 644 }}, 645 expectedNumSlices: 2, 646 expectedClientActions: 1, 647 expectedMetrics: &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 3, numCreated: 1}, 648 }, { 649 testName: "Endpoints with 2 subsets, multiple ports, and dual stack addresses", 650 subsets: []corev1.EndpointSubset{{ 651 Ports: []corev1.EndpointPort{{ 652 Name: "http", 653 Port: 80, 654 Protocol: corev1.ProtocolTCP, 655 }, { 656 Name: "https", 657 Port: 443, 658 Protocol: corev1.ProtocolUDP, 659 }}, 660 Addresses: []corev1.EndpointAddress{{ 661 IP: "2001:db8:2222:3333:4444:5555:6666:7777", 662 Hostname: "pod-1", 663 NodeName: pointer.String("node-1"), 664 }, { 665 IP: "10.0.0.2", 666 Hostname: "pod-2", 667 NodeName: pointer.String("node-2"), 668 }}, 669 }, { 670 Ports: []corev1.EndpointPort{{ 671 Name: "http", 672 Port: 3000, 673 Protocol: corev1.ProtocolTCP, 674 }, { 675 Name: "https", 676 Port: 3001, 677 Protocol: corev1.ProtocolUDP, 678 }}, 679 Addresses: []corev1.EndpointAddress{{ 680 IP: "10.0.1.1", 681 Hostname: "pod-11", 682 NodeName: pointer.String("node-1"), 683 }, { 684 IP: "10.0.1.2", 685 Hostname: "pod-12", 686 NodeName: pointer.String("node-2"), 687 }, { 688 IP: "2001:db8:3333:4444:5555:6666:7777:8888", 689 Hostname: "pod-13", 690 NodeName: pointer.String("node-3"), 691 }}, 692 }}, 693 existingEndpointSlices: []*discovery.EndpointSlice{}, 694 expectedNumSlices: 4, 695 expectedClientActions: 4, 696 expectedMetrics: &expectedMetrics{desiredSlices: 4, actualSlices: 4, desiredEndpoints: 5, addedPerSync: 5, numCreated: 4}, 697 }, { 698 testName: "Endpoints with 2 subsets, multiple ports, ipv6 only addresses", 699 subsets: []corev1.EndpointSubset{{ 700 Ports: []corev1.EndpointPort{{ 701 Name: "http", 702 Port: 80, 703 Protocol: corev1.ProtocolTCP, 704 }, { 705 Name: "https", 706 Port: 443, 707 Protocol: corev1.ProtocolUDP, 708 }}, 709 Addresses: []corev1.EndpointAddress{{ 710 IP: "2001:db8:1111:3333:4444:5555:6666:7777", 711 Hostname: "pod-1", 712 NodeName: pointer.String("node-1"), 713 }, { 714 IP: "2001:db8:2222:3333:4444:5555:6666:7777", 715 Hostname: "pod-2", 716 NodeName: pointer.String("node-2"), 717 }}, 718 }, { 719 Ports: []corev1.EndpointPort{{ 720 Name: "http", 721 Port: 3000, 722 Protocol: corev1.ProtocolTCP, 723 }, { 724 Name: "https", 725 Port: 3001, 726 Protocol: corev1.ProtocolUDP, 727 }}, 728 Addresses: []corev1.EndpointAddress{{ 729 IP: "2001:db8:3333:3333:4444:5555:6666:7777", 730 Hostname: "pod-11", 731 NodeName: pointer.String("node-1"), 732 }, { 733 IP: "2001:db8:4444:3333:4444:5555:6666:7777", 734 Hostname: "pod-12", 735 NodeName: pointer.String("node-2"), 736 }, { 737 IP: "2001:db8:5555:3333:4444:5555:6666:7777", 738 Hostname: "pod-13", 739 NodeName: pointer.String("node-3"), 740 }}, 741 }}, 742 existingEndpointSlices: []*discovery.EndpointSlice{}, 743 expectedNumSlices: 2, 744 expectedClientActions: 2, 745 expectedMetrics: &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 5, addedPerSync: 5, numCreated: 2}, 746 }, { 747 testName: "Endpoints with 2 subsets, multiple ports, some invalid addresses", 748 subsets: []corev1.EndpointSubset{{ 749 Ports: []corev1.EndpointPort{{ 750 Name: "http", 751 Port: 80, 752 Protocol: corev1.ProtocolTCP, 753 }, { 754 Name: "https", 755 Port: 443, 756 Protocol: corev1.ProtocolUDP, 757 }}, 758 Addresses: []corev1.EndpointAddress{{ 759 IP: "2001:db8:1111:3333:4444:5555:6666:7777", 760 Hostname: "pod-1", 761 NodeName: pointer.String("node-1"), 762 }, { 763 IP: "this-is-not-an-ip", 764 Hostname: "pod-2", 765 NodeName: pointer.String("node-2"), 766 }}, 767 }, { 768 Ports: []corev1.EndpointPort{{ 769 Name: "http", 770 Port: 3000, 771 Protocol: corev1.ProtocolTCP, 772 }, { 773 Name: "https", 774 Port: 3001, 775 Protocol: corev1.ProtocolUDP, 776 }}, 777 Addresses: []corev1.EndpointAddress{{ 778 IP: "this-is-also-not-an-ip", 779 Hostname: "pod-11", 780 NodeName: pointer.String("node-1"), 781 }, { 782 IP: "2001:db8:4444:3333:4444:5555:6666:7777", 783 Hostname: "pod-12", 784 NodeName: pointer.String("node-2"), 785 }, { 786 IP: "2001:db8:5555:3333:4444:5555:6666:7777", 787 Hostname: "pod-13", 788 NodeName: pointer.String("node-3"), 789 }}, 790 }}, 791 existingEndpointSlices: []*discovery.EndpointSlice{}, 792 expectedNumSlices: 2, 793 expectedClientActions: 2, 794 expectedMetrics: &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 3, addedPerSync: 3, skippedPerSync: 2, numCreated: 2}, 795 }, { 796 testName: "Endpoints with 2 subsets, multiple ports, all invalid addresses", 797 subsets: []corev1.EndpointSubset{{ 798 Ports: []corev1.EndpointPort{{ 799 Name: "http", 800 Port: 80, 801 Protocol: corev1.ProtocolTCP, 802 }, { 803 Name: "https", 804 Port: 443, 805 Protocol: corev1.ProtocolUDP, 806 }}, 807 Addresses: []corev1.EndpointAddress{{ 808 IP: "this-is-not-an-ip1", 809 Hostname: "pod-1", 810 NodeName: pointer.String("node-1"), 811 }, { 812 IP: "this-is-not-an-ip12", 813 Hostname: "pod-2", 814 NodeName: pointer.String("node-2"), 815 }}, 816 }, { 817 Ports: []corev1.EndpointPort{{ 818 Name: "http", 819 Port: 3000, 820 Protocol: corev1.ProtocolTCP, 821 }, { 822 Name: "https", 823 Port: 3001, 824 Protocol: corev1.ProtocolUDP, 825 }}, 826 Addresses: []corev1.EndpointAddress{{ 827 IP: "this-is-not-an-ip11", 828 Hostname: "pod-11", 829 NodeName: pointer.String("node-1"), 830 }, { 831 IP: "this-is-not-an-ip12", 832 Hostname: "pod-12", 833 NodeName: pointer.String("node-2"), 834 }, { 835 IP: "this-is-not-an-ip3", 836 Hostname: "pod-13", 837 NodeName: pointer.String("node-3"), 838 }}, 839 }}, 840 existingEndpointSlices: []*discovery.EndpointSlice{}, 841 expectedNumSlices: 0, 842 expectedClientActions: 0, 843 expectedMetrics: &expectedMetrics{desiredSlices: 0, actualSlices: 0, desiredEndpoints: 0, addedPerSync: 0, skippedPerSync: 5, numCreated: 0}, 844 }, { 845 testName: "Endpoints with 2 subsets, 1 exceeding maxEndpointsPerSubset", 846 subsets: []corev1.EndpointSubset{{ 847 Ports: []corev1.EndpointPort{{ 848 Name: "http", 849 Port: 80, 850 Protocol: corev1.ProtocolTCP, 851 }, { 852 Name: "https", 853 Port: 443, 854 Protocol: corev1.ProtocolUDP, 855 }}, 856 Addresses: []corev1.EndpointAddress{{ 857 IP: "10.0.0.1", 858 Hostname: "pod-1", 859 NodeName: pointer.String("node-1"), 860 }, { 861 IP: "10.0.0.2", 862 Hostname: "pod-2", 863 NodeName: pointer.String("node-2"), 864 }}, 865 }, { 866 Ports: []corev1.EndpointPort{{ 867 Name: "http", 868 Port: 3000, 869 Protocol: corev1.ProtocolTCP, 870 }, { 871 Name: "https", 872 Port: 3001, 873 Protocol: corev1.ProtocolUDP, 874 }}, 875 Addresses: []corev1.EndpointAddress{{ 876 IP: "10.0.1.1", 877 Hostname: "pod-11", 878 NodeName: pointer.String("node-1"), 879 }, { 880 IP: "10.0.1.2", 881 Hostname: "pod-12", 882 NodeName: pointer.String("node-2"), 883 }, { 884 IP: "10.0.1.3", 885 Hostname: "pod-13", 886 NodeName: pointer.String("node-3"), 887 }}, 888 }}, 889 existingEndpointSlices: []*discovery.EndpointSlice{}, 890 expectedNumSlices: 2, 891 expectedClientActions: 2, 892 maxEndpointsPerSubset: 2, 893 expectedMetrics: &expectedMetrics{desiredSlices: 2, actualSlices: 2, desiredEndpoints: 4, addedPerSync: 4, updatedPerSync: 0, removedPerSync: 0, skippedPerSync: 1, numCreated: 2, numUpdated: 0}, 894 }, { 895 testName: "The last-applied-configuration annotation should not get mirrored to created or updated endpoint slices", 896 epAnnotations: map[string]string{ 897 corev1.LastAppliedConfigAnnotation: "{\"apiVersion\":\"v1\",\"kind\":\"Endpoints\",\"subsets\":[]}", 898 }, 899 subsets: []corev1.EndpointSubset{{ 900 Ports: []corev1.EndpointPort{{ 901 Name: "http", 902 Port: 80, 903 Protocol: corev1.ProtocolTCP, 904 }}, 905 Addresses: []corev1.EndpointAddress{{ 906 IP: "10.0.0.1", 907 Hostname: "pod-1", 908 }}, 909 }}, 910 existingEndpointSlices: []*discovery.EndpointSlice{}, 911 expectedNumSlices: 1, 912 expectedClientActions: 1, 913 expectedMetrics: &expectedMetrics{addedPerSync: 1, numCreated: 1, desiredEndpoints: 1, desiredSlices: 1, actualSlices: 1}, 914 }, { 915 testName: "The last-applied-configuration annotation shouldn't get added to created endpoint slices", 916 subsets: []corev1.EndpointSubset{{ 917 Ports: []corev1.EndpointPort{{ 918 Name: "http", 919 Port: 80, 920 Protocol: corev1.ProtocolTCP, 921 }}, 922 Addresses: []corev1.EndpointAddress{{ 923 IP: "10.0.0.1", 924 Hostname: "pod-1", 925 }}, 926 }}, 927 existingEndpointSlices: []*discovery.EndpointSlice{}, 928 expectedNumSlices: 1, 929 expectedClientActions: 1, 930 expectedMetrics: &expectedMetrics{addedPerSync: 1, numCreated: 1, desiredEndpoints: 1, desiredSlices: 1, actualSlices: 1}, 931 }, { 932 testName: "The last-applied-configuration shouldn't get mirrored to endpoint slices when it's value is empty", 933 epAnnotations: map[string]string{ 934 corev1.LastAppliedConfigAnnotation: "", 935 }, 936 subsets: []corev1.EndpointSubset{{ 937 Ports: []corev1.EndpointPort{{ 938 Name: "http", 939 Port: 80, 940 Protocol: corev1.ProtocolTCP, 941 }}, 942 Addresses: []corev1.EndpointAddress{{ 943 IP: "10.0.0.1", 944 Hostname: "pod-1", 945 }}, 946 }}, 947 existingEndpointSlices: []*discovery.EndpointSlice{}, 948 expectedNumSlices: 1, 949 expectedClientActions: 1, 950 expectedMetrics: &expectedMetrics{addedPerSync: 1, numCreated: 1, desiredEndpoints: 1, desiredSlices: 1, actualSlices: 1}, 951 }, { 952 testName: "Annotations other than last-applied-configuration should get correctly mirrored", 953 epAnnotations: map[string]string{ 954 corev1.LastAppliedConfigAnnotation: "{\"apiVersion\":\"v1\",\"kind\":\"Endpoints\",\"subsets\":[]}", 955 "foo": "bar", 956 }, 957 subsets: []corev1.EndpointSubset{{ 958 Ports: []corev1.EndpointPort{{ 959 Name: "http", 960 Port: 80, 961 Protocol: corev1.ProtocolTCP, 962 }}, 963 Addresses: []corev1.EndpointAddress{{ 964 IP: "10.0.0.1", 965 Hostname: "pod-1", 966 }}, 967 }}, 968 existingEndpointSlices: []*discovery.EndpointSlice{}, 969 expectedNumSlices: 1, 970 expectedClientActions: 1, 971 expectedMetrics: &expectedMetrics{addedPerSync: 1, numCreated: 1, desiredEndpoints: 1, desiredSlices: 1, actualSlices: 1}, 972 }, { 973 testName: "Annotation mirroring should remove the last-applied-configuration annotation from existing endpoint slices", 974 subsets: []corev1.EndpointSubset{{ 975 Ports: []corev1.EndpointPort{{ 976 Name: "http", 977 Port: 80, 978 Protocol: corev1.ProtocolTCP, 979 }}, 980 Addresses: []corev1.EndpointAddress{{ 981 IP: "10.0.0.1", 982 Hostname: "pod-1", 983 }}, 984 }}, 985 existingEndpointSlices: []*discovery.EndpointSlice{{ 986 ObjectMeta: metav1.ObjectMeta{ 987 Name: "test-ep-1", 988 Annotations: map[string]string{ 989 corev1.LastAppliedConfigAnnotation: "{\"apiVersion\":\"v1\",\"kind\":\"Endpoints\",\"subsets\":[]}", 990 }, 991 }, 992 AddressType: discovery.AddressTypeIPv4, 993 Ports: []discovery.EndpointPort{{ 994 Name: pointer.String("http"), 995 Port: pointer.Int32(80), 996 Protocol: &protoTCP, 997 }}, 998 Endpoints: []discovery.Endpoint{{ 999 Addresses: []string{"10.0.0.1"}, 1000 Hostname: pointer.String("pod-1"), 1001 Conditions: discovery.EndpointConditions{Ready: pointer.Bool(true)}, 1002 }}, 1003 }}, 1004 expectedNumSlices: 1, 1005 expectedClientActions: 1, 1006 }} 1007 1008 for _, tc := range testCases { 1009 t.Run(tc.testName, func(t *testing.T) { 1010 client := newClientset() 1011 setupMetrics() 1012 namespace := "test" 1013 endpoints := corev1.Endpoints{ 1014 ObjectMeta: metav1.ObjectMeta{Name: "test-ep", Namespace: namespace, Labels: tc.epLabels, Annotations: tc.epAnnotations}, 1015 Subsets: tc.subsets, 1016 } 1017 1018 if tc.endpointsDeletionPending { 1019 now := metav1.Now() 1020 endpoints.DeletionTimestamp = &now 1021 } 1022 1023 numInitialActions := 0 1024 for _, epSlice := range tc.existingEndpointSlices { 1025 epSlice.Labels = map[string]string{ 1026 discovery.LabelServiceName: endpoints.Name, 1027 discovery.LabelManagedBy: controllerName, 1028 } 1029 _, err := client.DiscoveryV1().EndpointSlices(namespace).Create(context.TODO(), epSlice, metav1.CreateOptions{}) 1030 if err != nil { 1031 t.Fatalf("Expected no error creating EndpointSlice, got %v", err) 1032 } 1033 numInitialActions++ 1034 } 1035 1036 maxEndpointsPerSubset := tc.maxEndpointsPerSubset 1037 if maxEndpointsPerSubset == 0 { 1038 maxEndpointsPerSubset = defaultMaxEndpointsPerSubset 1039 } 1040 r := newReconciler(client, maxEndpointsPerSubset) 1041 reconcileHelper(t, r, &endpoints, tc.existingEndpointSlices) 1042 1043 numExtraActions := len(client.Actions()) - numInitialActions 1044 if numExtraActions != tc.expectedClientActions { 1045 t.Fatalf("Expected %d additional client actions, got %d: %#v", tc.expectedClientActions, numExtraActions, client.Actions()[numInitialActions:]) 1046 } 1047 1048 if tc.expectedMetrics != nil { 1049 expectMetrics(t, *tc.expectedMetrics) 1050 } 1051 1052 endpointSlices := fetchEndpointSlices(t, client, namespace) 1053 expectEndpointSlices(t, tc.expectedNumSlices, int(maxEndpointsPerSubset), endpoints, endpointSlices) 1054 }) 1055 } 1056 } 1057 1058 // Test Helpers 1059 1060 func newReconciler(client *fake.Clientset, maxEndpointsPerSubset int32) *reconciler { 1061 broadcaster := record.NewBroadcaster() 1062 recorder := broadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: "endpoint-slice-mirroring-controller"}) 1063 1064 return &reconciler{ 1065 client: client, 1066 maxEndpointsPerSubset: maxEndpointsPerSubset, 1067 endpointSliceTracker: endpointsliceutil.NewEndpointSliceTracker(), 1068 metricsCache: metrics.NewCache(maxEndpointsPerSubset), 1069 eventRecorder: recorder, 1070 } 1071 } 1072 1073 func expectEndpointSlices(t *testing.T, num, maxEndpointsPerSubset int, endpoints corev1.Endpoints, endpointSlices []discovery.EndpointSlice) { 1074 t.Helper() 1075 if len(endpointSlices) != num { 1076 t.Fatalf("Expected %d EndpointSlices, got %d", num, len(endpointSlices)) 1077 } 1078 1079 if num == 0 { 1080 return 1081 } 1082 1083 for _, epSlice := range endpointSlices { 1084 if !strings.HasPrefix(epSlice.Name, endpoints.Name) { 1085 t.Errorf("Expected EndpointSlice name to start with %s, got %s", endpoints.Name, epSlice.Name) 1086 } 1087 1088 serviceNameVal, ok := epSlice.Labels[discovery.LabelServiceName] 1089 if !ok { 1090 t.Errorf("Expected EndpointSlice to have %s label set", discovery.LabelServiceName) 1091 } 1092 if serviceNameVal != endpoints.Name { 1093 t.Errorf("Expected EndpointSlice to have %s label set to %s, got %s", discovery.LabelServiceName, endpoints.Name, serviceNameVal) 1094 } 1095 1096 _, ok = epSlice.Annotations[corev1.LastAppliedConfigAnnotation] 1097 if ok { 1098 t.Errorf("Expected LastAppliedConfigAnnotation to be unset, got %s", epSlice.Annotations[corev1.LastAppliedConfigAnnotation]) 1099 } 1100 1101 _, ok = epSlice.Annotations[corev1.EndpointsLastChangeTriggerTime] 1102 if ok { 1103 t.Errorf("Expected EndpointsLastChangeTriggerTime to be unset, got %s", epSlice.Annotations[corev1.EndpointsLastChangeTriggerTime]) 1104 } 1105 1106 for annotation, val := range endpoints.Annotations { 1107 if annotation == corev1.EndpointsLastChangeTriggerTime || annotation == corev1.LastAppliedConfigAnnotation { 1108 continue 1109 } 1110 if epSlice.Annotations[annotation] != val { 1111 t.Errorf("Expected endpoint annotation %s to be mirrored correctly, got %s", annotation, epSlice.Annotations[annotation]) 1112 } 1113 } 1114 } 1115 1116 // canonicalize endpoints to match the expected endpoints, otherwise the test 1117 // that creates more endpoints than allowed fail becaused the list of final 1118 // endpoints doesn't match. 1119 for _, epSubset := range endpointsv1.RepackSubsets(endpoints.Subsets) { 1120 if len(epSubset.Addresses) == 0 && len(epSubset.NotReadyAddresses) == 0 { 1121 continue 1122 } 1123 1124 var matchingEndpointsV4, matchingEndpointsV6 []discovery.Endpoint 1125 1126 for _, epSlice := range endpointSlices { 1127 if portsMatch(epSubset.Ports, epSlice.Ports) { 1128 switch epSlice.AddressType { 1129 case discovery.AddressTypeIPv4: 1130 matchingEndpointsV4 = append(matchingEndpointsV4, epSlice.Endpoints...) 1131 case discovery.AddressTypeIPv6: 1132 matchingEndpointsV6 = append(matchingEndpointsV6, epSlice.Endpoints...) 1133 default: 1134 t.Fatalf("Unexpected EndpointSlice address type found: %v", epSlice.AddressType) 1135 } 1136 } 1137 } 1138 1139 if len(matchingEndpointsV4) == 0 && len(matchingEndpointsV6) == 0 { 1140 t.Fatalf("No EndpointSlices match Endpoints subset: %#v", epSubset.Ports) 1141 } 1142 1143 expectMatchingAddresses(t, epSubset, matchingEndpointsV4, discovery.AddressTypeIPv4, maxEndpointsPerSubset) 1144 expectMatchingAddresses(t, epSubset, matchingEndpointsV6, discovery.AddressTypeIPv6, maxEndpointsPerSubset) 1145 } 1146 } 1147 1148 func portsMatch(epPorts []corev1.EndpointPort, epsPorts []discovery.EndpointPort) bool { 1149 if len(epPorts) != len(epsPorts) { 1150 return false 1151 } 1152 1153 portsToBeMatched := map[int32]corev1.EndpointPort{} 1154 1155 for _, epPort := range epPorts { 1156 portsToBeMatched[epPort.Port] = epPort 1157 } 1158 1159 for _, epsPort := range epsPorts { 1160 epPort, ok := portsToBeMatched[*epsPort.Port] 1161 if !ok { 1162 return false 1163 } 1164 delete(portsToBeMatched, *epsPort.Port) 1165 1166 if epPort.Name != *epsPort.Name { 1167 return false 1168 } 1169 if epPort.Port != *epsPort.Port { 1170 return false 1171 } 1172 if epPort.Protocol != *epsPort.Protocol { 1173 return false 1174 } 1175 if epPort.AppProtocol != epsPort.AppProtocol { 1176 return false 1177 } 1178 } 1179 1180 return true 1181 } 1182 1183 func expectMatchingAddresses(t *testing.T, epSubset corev1.EndpointSubset, esEndpoints []discovery.Endpoint, addrType discovery.AddressType, maxEndpointsPerSubset int) { 1184 t.Helper() 1185 type addressInfo struct { 1186 ready bool 1187 epAddress corev1.EndpointAddress 1188 } 1189 1190 // This approach assumes that each IP is unique within an EndpointSubset. 1191 expectedEndpoints := map[string]addressInfo{} 1192 1193 for _, address := range epSubset.Addresses { 1194 at := getAddressType(address.IP) 1195 if at != nil && *at == addrType && len(expectedEndpoints) < maxEndpointsPerSubset { 1196 expectedEndpoints[address.IP] = addressInfo{ 1197 ready: true, 1198 epAddress: address, 1199 } 1200 } 1201 } 1202 1203 for _, address := range epSubset.NotReadyAddresses { 1204 at := getAddressType(address.IP) 1205 if at != nil && *at == addrType && len(expectedEndpoints) < maxEndpointsPerSubset { 1206 expectedEndpoints[address.IP] = addressInfo{ 1207 ready: false, 1208 epAddress: address, 1209 } 1210 } 1211 } 1212 1213 if len(expectedEndpoints) != len(esEndpoints) { 1214 t.Errorf("Expected %d endpoints, got %d", len(expectedEndpoints), len(esEndpoints)) 1215 } 1216 1217 for _, endpoint := range esEndpoints { 1218 if len(endpoint.Addresses) != 1 { 1219 t.Fatalf("Expected endpoint to have 1 address, got %d", len(endpoint.Addresses)) 1220 } 1221 address := endpoint.Addresses[0] 1222 expectedEndpoint, ok := expectedEndpoints[address] 1223 1224 if !ok { 1225 t.Fatalf("EndpointSlice has endpoint with unexpected address: %s", address) 1226 } 1227 1228 if expectedEndpoint.ready != *endpoint.Conditions.Ready { 1229 t.Errorf("Expected ready to be %t, got %t", expectedEndpoint.ready, *endpoint.Conditions.Ready) 1230 } 1231 1232 if endpoint.Hostname == nil { 1233 if expectedEndpoint.epAddress.Hostname != "" { 1234 t.Errorf("Expected hostname to be %s, got nil", expectedEndpoint.epAddress.Hostname) 1235 } 1236 } else if expectedEndpoint.epAddress.Hostname != *endpoint.Hostname { 1237 t.Errorf("Expected hostname to be %s, got %s", expectedEndpoint.epAddress.Hostname, *endpoint.Hostname) 1238 } 1239 1240 if expectedEndpoint.epAddress.NodeName != nil { 1241 if endpoint.NodeName == nil { 1242 t.Errorf("Expected nodeName to be set") 1243 } 1244 if *expectedEndpoint.epAddress.NodeName != *endpoint.NodeName { 1245 t.Errorf("Expected nodeName to be %s, got %s", *expectedEndpoint.epAddress.NodeName, *endpoint.NodeName) 1246 } 1247 } 1248 } 1249 } 1250 1251 func fetchEndpointSlices(t *testing.T, client *fake.Clientset, namespace string) []discovery.EndpointSlice { 1252 t.Helper() 1253 fetchedSlices, err := client.DiscoveryV1().EndpointSlices(namespace).List(context.TODO(), metav1.ListOptions{ 1254 LabelSelector: discovery.LabelManagedBy + "=" + controllerName, 1255 }) 1256 if err != nil { 1257 t.Fatalf("Expected no error fetching Endpoint Slices, got: %v", err) 1258 return []discovery.EndpointSlice{} 1259 } 1260 return fetchedSlices.Items 1261 } 1262 1263 func reconcileHelper(t *testing.T, r *reconciler, endpoints *corev1.Endpoints, existingSlices []*discovery.EndpointSlice) { 1264 t.Helper() 1265 logger, _ := ktesting.NewTestContext(t) 1266 err := r.reconcile(logger, endpoints, existingSlices) 1267 if err != nil { 1268 t.Fatalf("Expected no error reconciling Endpoint Slices, got: %v", err) 1269 } 1270 } 1271 1272 // Metrics helpers 1273 1274 type expectedMetrics struct { 1275 desiredSlices int 1276 actualSlices int 1277 desiredEndpoints int 1278 addedPerSync int 1279 updatedPerSync int 1280 removedPerSync int 1281 skippedPerSync int 1282 numCreated int 1283 numUpdated int 1284 numDeleted int 1285 } 1286 1287 func expectMetrics(t *testing.T, em expectedMetrics) { 1288 t.Helper() 1289 1290 actualDesiredSlices, err := testutil.GetGaugeMetricValue(metrics.DesiredEndpointSlices.WithLabelValues()) 1291 handleErr(t, err, "desiredEndpointSlices") 1292 if actualDesiredSlices != float64(em.desiredSlices) { 1293 t.Errorf("Expected desiredEndpointSlices to be %d, got %v", em.desiredSlices, actualDesiredSlices) 1294 } 1295 1296 actualNumSlices, err := testutil.GetGaugeMetricValue(metrics.NumEndpointSlices.WithLabelValues()) 1297 handleErr(t, err, "numEndpointSlices") 1298 if actualNumSlices != float64(em.actualSlices) { 1299 t.Errorf("Expected numEndpointSlices to be %d, got %v", em.actualSlices, actualNumSlices) 1300 } 1301 1302 actualEndpointsDesired, err := testutil.GetGaugeMetricValue(metrics.EndpointsDesired.WithLabelValues()) 1303 handleErr(t, err, "desiredEndpoints") 1304 if actualEndpointsDesired != float64(em.desiredEndpoints) { 1305 t.Errorf("Expected desiredEndpoints to be %d, got %v", em.desiredEndpoints, actualEndpointsDesired) 1306 } 1307 1308 actualAddedPerSync, err := testutil.GetHistogramMetricValue(metrics.EndpointsAddedPerSync.WithLabelValues()) 1309 handleErr(t, err, "endpointsAddedPerSync") 1310 if actualAddedPerSync != float64(em.addedPerSync) { 1311 t.Errorf("Expected endpointsAddedPerSync to be %d, got %v", em.addedPerSync, actualAddedPerSync) 1312 } 1313 1314 actualUpdatedPerSync, err := testutil.GetHistogramMetricValue(metrics.EndpointsUpdatedPerSync.WithLabelValues()) 1315 handleErr(t, err, "endpointsUpdatedPerSync") 1316 if actualUpdatedPerSync != float64(em.updatedPerSync) { 1317 t.Errorf("Expected endpointsUpdatedPerSync to be %d, got %v", em.updatedPerSync, actualUpdatedPerSync) 1318 } 1319 1320 actualRemovedPerSync, err := testutil.GetHistogramMetricValue(metrics.EndpointsRemovedPerSync.WithLabelValues()) 1321 handleErr(t, err, "endpointsRemovedPerSync") 1322 if actualRemovedPerSync != float64(em.removedPerSync) { 1323 t.Errorf("Expected endpointsRemovedPerSync to be %d, got %v", em.removedPerSync, actualRemovedPerSync) 1324 } 1325 1326 actualSkippedPerSync, err := testutil.GetHistogramMetricValue(metrics.AddressesSkippedPerSync.WithLabelValues()) 1327 handleErr(t, err, "addressesSkippedPerSync") 1328 if actualSkippedPerSync != float64(em.skippedPerSync) { 1329 t.Errorf("Expected addressesSkippedPerSync to be %d, got %v", em.skippedPerSync, actualSkippedPerSync) 1330 } 1331 1332 actualCreated, err := testutil.GetCounterMetricValue(metrics.EndpointSliceChanges.WithLabelValues("create")) 1333 handleErr(t, err, "endpointSliceChangesCreated") 1334 if actualCreated != float64(em.numCreated) { 1335 t.Errorf("Expected endpointSliceChangesCreated to be %d, got %v", em.numCreated, actualCreated) 1336 } 1337 1338 actualUpdated, err := testutil.GetCounterMetricValue(metrics.EndpointSliceChanges.WithLabelValues("update")) 1339 handleErr(t, err, "endpointSliceChangesUpdated") 1340 if actualUpdated != float64(em.numUpdated) { 1341 t.Errorf("Expected endpointSliceChangesUpdated to be %d, got %v", em.numUpdated, actualUpdated) 1342 } 1343 1344 actualDeleted, err := testutil.GetCounterMetricValue(metrics.EndpointSliceChanges.WithLabelValues("delete")) 1345 handleErr(t, err, "desiredEndpointSlices") 1346 if actualDeleted != float64(em.numDeleted) { 1347 t.Errorf("Expected endpointSliceChangesDeleted to be %d, got %v", em.numDeleted, actualDeleted) 1348 } 1349 } 1350 1351 func handleErr(t *testing.T, err error, metricName string) { 1352 if err != nil { 1353 t.Errorf("Failed to get %s value, err: %v", metricName, err) 1354 } 1355 } 1356 1357 func setupMetrics() { 1358 metrics.RegisterMetrics() 1359 metrics.NumEndpointSlices.Delete(map[string]string{}) 1360 metrics.DesiredEndpointSlices.Delete(map[string]string{}) 1361 metrics.EndpointsDesired.Delete(map[string]string{}) 1362 metrics.EndpointsAddedPerSync.Delete(map[string]string{}) 1363 metrics.EndpointsUpdatedPerSync.Delete(map[string]string{}) 1364 metrics.EndpointsRemovedPerSync.Delete(map[string]string{}) 1365 metrics.AddressesSkippedPerSync.Delete(map[string]string{}) 1366 metrics.EndpointSliceChanges.Delete(map[string]string{"operation": "create"}) 1367 metrics.EndpointSliceChanges.Delete(map[string]string{"operation": "update"}) 1368 metrics.EndpointSliceChanges.Delete(map[string]string{"operation": "delete"}) 1369 }