github.com/cilium/cilium@v1.16.2/pkg/bgpv1/test/adverts_test.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package test 5 6 import ( 7 "context" 8 "reflect" 9 "strconv" 10 "strings" 11 "testing" 12 "time" 13 14 "github.com/osrg/gobgp/v3/pkg/packet/bgp" 15 "github.com/stretchr/testify/require" 16 meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 17 "k8s.io/utils/pointer" 18 19 ipam_option "github.com/cilium/cilium/pkg/ipam/option" 20 ipam_types "github.com/cilium/cilium/pkg/ipam/types" 21 v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2" 22 v2alpha1 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1" 23 slim_metav1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1" 24 "github.com/cilium/cilium/pkg/testutils" 25 ) 26 27 var ( 28 // maxTestDuration is allowed time for test execution 29 maxTestDuration = 15 * time.Second 30 31 // maxGracefulRestartTestDuration is max allowed time for graceful restart test 32 maxGracefulRestartTestDuration = 1 * time.Minute 33 ) 34 35 // Test_PodCIDRAdvert validates pod IPv4/v6 subnet is advertised, withdrawn and modified on node addresses change. 36 func Test_PodCIDRAdvert(t *testing.T) { 37 testutils.PrivilegedTest(t) 38 39 // steps define order in which test is run. Note, this is different from table tests, in which each unit is 40 // independent. In this case, tests are run sequentially and there is dependency on previous test step. 41 var steps = []struct { 42 description string 43 podCIDRs []string 44 expectedRouteEvents []routeEvent 45 }{ 46 { 47 description: "advertise pod CIDRs", 48 podCIDRs: []string{ 49 "10.1.0.0/16", 50 "aaaa::/64", 51 }, 52 expectedRouteEvents: []routeEvent{ 53 { 54 sourceASN: ciliumASN, 55 prefix: "10.1.0.0", 56 prefixLen: 16, 57 isWithdrawn: false, 58 }, 59 { 60 sourceASN: ciliumASN, 61 prefix: "aaaa::", 62 prefixLen: 64, 63 isWithdrawn: false, 64 }, 65 }, 66 }, 67 { 68 description: "delete pod CIDRs", 69 podCIDRs: []string{}, 70 expectedRouteEvents: []routeEvent{ 71 { 72 sourceASN: ciliumASN, 73 prefix: "10.1.0.0", 74 prefixLen: 16, 75 isWithdrawn: true, 76 }, 77 { 78 sourceASN: ciliumASN, 79 prefix: "aaaa::", 80 prefixLen: 64, 81 isWithdrawn: true, 82 }, 83 }, 84 }, 85 { 86 description: "re-add pod CIDRs", 87 podCIDRs: []string{ 88 "10.1.0.0/16", 89 "aaaa::/64", 90 }, 91 expectedRouteEvents: []routeEvent{ 92 { 93 sourceASN: ciliumASN, 94 prefix: "10.1.0.0", 95 prefixLen: 16, 96 isWithdrawn: false, 97 }, 98 { 99 sourceASN: ciliumASN, 100 prefix: "aaaa::", 101 prefixLen: 64, 102 isWithdrawn: false, 103 }, 104 }, 105 }, 106 { 107 description: "update pod CIDRs", 108 podCIDRs: []string{ 109 "10.2.0.0/16", 110 "bbbb::/64", 111 }, 112 expectedRouteEvents: []routeEvent{ 113 { 114 sourceASN: ciliumASN, 115 prefix: "10.1.0.0", 116 prefixLen: 16, 117 isWithdrawn: true, 118 }, 119 { 120 sourceASN: ciliumASN, 121 prefix: "10.2.0.0", 122 prefixLen: 16, 123 isWithdrawn: false, 124 }, 125 { 126 sourceASN: ciliumASN, 127 prefix: "aaaa::", 128 prefixLen: 64, 129 isWithdrawn: true, 130 }, 131 { 132 sourceASN: ciliumASN, 133 prefix: "bbbb::", 134 prefixLen: 64, 135 isWithdrawn: false, 136 }, 137 }, 138 }, 139 } 140 141 testCtx, testDone := context.WithTimeout(context.Background(), maxTestDuration) 142 defer testDone() 143 144 // setup topology 145 gobgpPeers, fixture, cleanup, err := setup(testCtx, t, []gobgpConfig{gobgpConf}, newFixtureConf()) 146 require.NoError(t, err) 147 require.Len(t, gobgpPeers, 1) 148 defer cleanup() 149 150 // setup neighbor 151 err = setupSingleNeighbor(testCtx, fixture, gobgpASN) 152 require.NoError(t, err) 153 154 // wait for peering to come up 155 err = gobgpPeers[0].waitForSessionState(testCtx, []string{"ESTABLISHED"}) 156 require.NoError(t, err) 157 158 tracker := fixture.fakeClientSet.CiliumFakeClientset.Tracker() 159 obj, err := tracker.Get(v2.SchemeGroupVersion.WithResource("ciliumnodes"), "", baseNode.name) 160 require.NoError(t, err) 161 node, ok := obj.(*v2.CiliumNode) 162 require.True(t, ok) 163 164 for _, step := range steps { 165 t.Run(step.description, func(t *testing.T) { 166 // update CiliumNode with new PodCIDR 167 // this will trigger a reconciliation as the controller is observing 168 // the local CiliumNode 169 node.Spec.IPAM.PodCIDRs = step.podCIDRs 170 err = tracker.Update(v2.SchemeGroupVersion.WithResource("ciliumnodes"), node, "") 171 require.NoError(t, err) 172 173 // validate expected result 174 receivedEvents, err := gobgpPeers[0].getRouteEvents(testCtx, len(step.expectedRouteEvents)) 175 require.NoError(t, err, step.description) 176 177 // match events in any order 178 require.ElementsMatch(t, step.expectedRouteEvents, receivedEvents, step.description) 179 }) 180 } 181 } 182 183 // Test_PodIPPoolAdvert validates pod ip pools are advertised to BGP peers. 184 func Test_PodIPPoolAdvert(t *testing.T) { 185 testutils.PrivilegedTest(t) 186 187 // Steps define the order that tests are run. Note, this is different from table tests, 188 // in which each unit is independent. In this case, tests are run sequentially and there 189 // is dependency on previous test step. 190 var steps = []struct { 191 name string 192 ipPoolOp string // "add" / "update" / "delete" 193 ipPools []ipam_types.IPAMPoolAllocation 194 poolLabels map[string]string 195 nodePools []ipam_types.IPAMPoolAllocation 196 poolSelector *slim_metav1.LabelSelector 197 expected []routeEvent 198 }{ 199 { 200 name: "nil pool labels", 201 ipPoolOp: "add", 202 ipPools: []ipam_types.IPAMPoolAllocation{ 203 { 204 Pool: "pool1", 205 CIDRs: []ipam_types.IPAMPodCIDR{"10.1.0.0/16"}, 206 }, 207 }, 208 poolLabels: nil, 209 nodePools: []ipam_types.IPAMPoolAllocation{ 210 { 211 Pool: "pool1", 212 CIDRs: []ipam_types.IPAMPodCIDR{"10.1.1.0/24"}, 213 }, 214 }, 215 poolSelector: &slim_metav1.LabelSelector{ 216 MatchLabels: map[string]string{"no": "pool-labels"}, 217 }, 218 expected: []routeEvent{}, 219 }, 220 { 221 name: "nil node pools", 222 ipPoolOp: "update", 223 ipPools: []ipam_types.IPAMPoolAllocation{ 224 { 225 Pool: "pool1", 226 CIDRs: []ipam_types.IPAMPodCIDR{"10.1.0.0/16"}, 227 }, 228 }, 229 poolLabels: map[string]string{"no": "node-cidrs"}, 230 nodePools: nil, 231 poolSelector: &slim_metav1.LabelSelector{ 232 MatchLabels: map[string]string{"no": "node-cidrs"}, 233 }, 234 expected: []routeEvent{}, 235 }, 236 { 237 name: "advertise matching ipv4 pool", 238 ipPoolOp: "update", 239 ipPools: []ipam_types.IPAMPoolAllocation{ 240 { 241 Pool: "pool1", 242 CIDRs: []ipam_types.IPAMPodCIDR{"10.1.0.0/16"}, 243 }, 244 }, 245 poolLabels: map[string]string{"label": "matched"}, 246 nodePools: []ipam_types.IPAMPoolAllocation{ 247 { 248 Pool: "pool1", 249 CIDRs: []ipam_types.IPAMPodCIDR{"10.1.1.0/24"}, 250 }, 251 }, 252 poolSelector: &slim_metav1.LabelSelector{ 253 MatchLabels: map[string]string{"label": "matched"}, 254 }, 255 expected: []routeEvent{ 256 { 257 sourceASN: ciliumASN, 258 prefix: "10.1.1.0", 259 prefixLen: 24, 260 isWithdrawn: false, 261 }, 262 }, 263 }, 264 { 265 name: "withdraw existing ipv4 pool", 266 ipPoolOp: "delete", 267 ipPools: []ipam_types.IPAMPoolAllocation{ 268 { 269 Pool: "pool1", 270 }, 271 }, 272 poolLabels: map[string]string{"label": "matched"}, 273 nodePools: nil, 274 poolSelector: &slim_metav1.LabelSelector{ 275 MatchLabels: map[string]string{"label": "matched"}, 276 }, 277 expected: []routeEvent{ 278 { 279 sourceASN: ciliumASN, 280 prefix: "10.1.1.0", 281 prefixLen: 24, 282 isWithdrawn: true, 283 }, 284 }, 285 }, 286 { 287 name: "advertise new ipv4 pool", 288 ipPoolOp: "add", 289 ipPools: []ipam_types.IPAMPoolAllocation{ 290 { 291 Pool: "pool1", 292 CIDRs: []ipam_types.IPAMPodCIDR{"11.2.0.0/16"}, 293 }, 294 }, 295 poolLabels: map[string]string{"label": "matched"}, 296 nodePools: []ipam_types.IPAMPoolAllocation{ 297 { 298 Pool: "pool1", 299 CIDRs: []ipam_types.IPAMPodCIDR{"11.2.1.0/24"}, 300 }, 301 }, 302 poolSelector: &slim_metav1.LabelSelector{ 303 MatchLabels: map[string]string{"label": "matched"}, 304 }, 305 expected: []routeEvent{ 306 { 307 sourceASN: ciliumASN, 308 prefix: "11.2.1.0", 309 prefixLen: 24, 310 isWithdrawn: false, 311 }, 312 }, 313 }, 314 { 315 name: "withdraw new ipv4 pool", 316 ipPoolOp: "delete", 317 ipPools: []ipam_types.IPAMPoolAllocation{ 318 { 319 Pool: "pool1", 320 }, 321 }, 322 poolLabels: map[string]string{"label": "matched"}, 323 nodePools: nil, 324 poolSelector: &slim_metav1.LabelSelector{ 325 MatchLabels: map[string]string{"label": "matched"}, 326 }, 327 expected: []routeEvent{ 328 { 329 sourceASN: ciliumASN, 330 prefix: "11.2.1.0", 331 prefixLen: 24, 332 isWithdrawn: true, 333 }, 334 }, 335 }, 336 { 337 name: "advertise matching ipv6 pool", 338 ipPoolOp: "add", 339 ipPools: []ipam_types.IPAMPoolAllocation{ 340 { 341 Pool: "pool1", 342 CIDRs: []ipam_types.IPAMPodCIDR{"2001:0:0:1234::/64"}, 343 }, 344 }, 345 poolLabels: map[string]string{"label": "matched"}, 346 nodePools: []ipam_types.IPAMPoolAllocation{ 347 { 348 Pool: "pool1", 349 CIDRs: []ipam_types.IPAMPodCIDR{"2001:0:0:1234:5678::/96"}, 350 }, 351 }, 352 poolSelector: &slim_metav1.LabelSelector{ 353 MatchLabels: map[string]string{"label": "matched"}, 354 }, 355 expected: []routeEvent{ 356 { 357 sourceASN: ciliumASN, 358 prefix: "2001:0:0:1234:5678::", 359 prefixLen: 96, 360 isWithdrawn: false, 361 }, 362 }, 363 }, 364 { 365 name: "withdraw existing ipv6 pool", 366 ipPoolOp: "delete", 367 ipPools: []ipam_types.IPAMPoolAllocation{ 368 { 369 Pool: "pool1", 370 }, 371 }, 372 poolLabels: map[string]string{"label": "matched"}, 373 nodePools: nil, 374 poolSelector: &slim_metav1.LabelSelector{ 375 MatchLabels: map[string]string{"label": "matched"}, 376 }, 377 expected: []routeEvent{ 378 { 379 sourceASN: ciliumASN, 380 prefix: "2001:0:0:1234:5678::", 381 prefixLen: 96, 382 isWithdrawn: true, 383 }, 384 }, 385 }, 386 { 387 name: "advertise new ipv6 pool", 388 ipPoolOp: "add", 389 ipPools: []ipam_types.IPAMPoolAllocation{ 390 { 391 Pool: "pool1", 392 CIDRs: []ipam_types.IPAMPodCIDR{"2002:0:0:1234::/64"}, 393 }, 394 }, 395 poolLabels: map[string]string{"label": "matched"}, 396 nodePools: []ipam_types.IPAMPoolAllocation{ 397 { 398 Pool: "pool1", 399 CIDRs: []ipam_types.IPAMPodCIDR{"2002:0:0:1234:5678::/96"}, 400 }, 401 }, 402 poolSelector: &slim_metav1.LabelSelector{ 403 MatchLabels: map[string]string{"label": "matched"}, 404 }, 405 expected: []routeEvent{ 406 { 407 sourceASN: ciliumASN, 408 prefix: "2002:0:0:1234:5678::", 409 prefixLen: 96, 410 isWithdrawn: false, 411 }, 412 }, 413 }, 414 } 415 416 testCtx, testDone := context.WithTimeout(context.Background(), maxTestDuration) 417 defer testDone() 418 419 // setup topology 420 cfg := newFixtureConf() 421 cfg.ipam = ipam_option.IPAMMultiPool 422 gobgpPeers, fixture, cleanup, err := setup(testCtx, t, []gobgpConfig{gobgpConf}, cfg) 423 require.NoError(t, err) 424 require.Len(t, gobgpPeers, 1) 425 defer cleanup() 426 427 // setup neighbor 428 err = setupSingleNeighbor(testCtx, fixture, gobgpASN) 429 require.NoError(t, err) 430 431 // wait for peering to establish 432 err = gobgpPeers[0].waitForSessionState(testCtx, []string{"ESTABLISHED"}) 433 require.NoError(t, err) 434 435 tracker := fixture.fakeClientSet.CiliumFakeClientset.Tracker() 436 437 for _, step := range steps { 438 t.Run(step.name, func(t *testing.T) { 439 // Setup pod ip pool objects with test case pool cidrs. 440 var poolObjs []*v2alpha1.CiliumPodIPPool 441 for _, pool := range step.ipPools { 442 var confCIDRs []ipam_types.IPAMPodCIDR 443 confCIDRs = append(confCIDRs, pool.CIDRs...) 444 poolObj := newIPPoolObj(ipPoolConfig{ 445 name: pool.Pool, 446 labels: step.poolLabels, 447 cidrs: confCIDRs, 448 }) 449 poolObjs = append(poolObjs, poolObj) 450 } 451 452 // Add / update / delete the pod ip pool object in the object tracker. 453 for _, obj := range poolObjs { 454 switch step.ipPoolOp { 455 case "add": 456 err = tracker.Add(obj) 457 case "update": 458 err = tracker.Update(v2alpha1.SchemeGroupVersion.WithResource("ciliumpodippools"), obj, "") 459 case "delete": 460 err = tracker.Delete(v2alpha1.SchemeGroupVersion.WithResource("ciliumpodippools"), "", obj.Name) 461 } 462 require.NoError(t, err) 463 } 464 465 // get the local CiliumNode 466 obj, err := tracker.Get(v2.SchemeGroupVersion.WithResource("ciliumnodes"), "", baseNode.name) 467 require.NoError(t, err) 468 node, ok := obj.(*v2.CiliumNode) 469 require.True(t, ok) 470 471 // update the local CiliumNode with the test case node ipam pools 472 node.Spec.IPAM.Pools.Allocated = step.nodePools 473 err = tracker.Update(v2.SchemeGroupVersion.WithResource("ciliumnodes"), node, "") 474 require.NoError(t, err) 475 476 // Setup the bgp policy object with the test case pool selector. 477 fixture.config.policy.Spec.VirtualRouters[0].PodIPPoolSelector = step.poolSelector 478 _, err = fixture.policyClient.Update(testCtx, &fixture.config.policy, meta_v1.UpdateOptions{}) 479 require.NoError(t, err) 480 481 receivedRouteMatch := func() bool { 482 // Validate the expected result. 483 receivedEvents, err := gobgpPeers[0].getRouteEvents(testCtx, len(step.expected)) 484 require.NoError(t, err, step.name) 485 if len(step.expected) == 0 && len(receivedEvents) == 0 { 486 return true 487 } 488 equal := reflect.DeepEqual(step.expected, receivedEvents) 489 if !equal { 490 t.Logf("route events not (yet) equal - expected: %v, actual: %v", step.expected, receivedEvents) 491 } 492 return equal 493 } 494 495 deadline, _ := testCtx.Deadline() 496 outstanding := time.Until(deadline) 497 require.Greater(t, outstanding, 0*time.Second, "test context deadline exceeded") 498 499 // Retry receivedRouteMatch until the test context deadline. 500 require.Eventually(t, receivedRouteMatch, outstanding, 100*time.Millisecond) 501 }) 502 } 503 } 504 505 // Test_LBEgressAdvertisementWithLoadBalancerIP validates Service v4 and v6 IPs is advertised, withdrawn and modified on changing policy. 506 func Test_LBEgressAdvertisementWithLoadBalancerIP(t *testing.T) { 507 testutils.PrivilegedTest(t) 508 509 var steps = []struct { 510 description string 511 srvName string 512 ingressIP string 513 op string // add or update 514 expectedRouteEvents []routeEvent 515 }{ 516 { 517 description: "advertise service IP", 518 srvName: "service-a", 519 ingressIP: "10.100.1.1", 520 op: "add", 521 expectedRouteEvents: []routeEvent{ 522 { 523 sourceASN: ciliumASN, 524 prefix: "10.100.1.1", 525 prefixLen: 32, 526 isWithdrawn: false, 527 }, 528 }, 529 }, 530 { 531 description: "withdraw service IP", 532 srvName: "service-a", 533 ingressIP: "", 534 op: "update", 535 expectedRouteEvents: []routeEvent{ 536 { 537 sourceASN: ciliumASN, 538 prefix: "10.100.1.1", 539 prefixLen: 32, 540 isWithdrawn: true, 541 }, 542 }, 543 }, 544 { 545 description: "re-advertise service IP", 546 srvName: "service-a", 547 ingressIP: "10.100.1.1", 548 op: "update", 549 expectedRouteEvents: []routeEvent{ 550 { 551 sourceASN: ciliumASN, 552 prefix: "10.100.1.1", 553 prefixLen: 32, 554 isWithdrawn: false, 555 }, 556 }, 557 }, 558 { 559 description: "update service IP", 560 srvName: "service-a", 561 ingressIP: "10.200.1.1", 562 op: "update", 563 expectedRouteEvents: []routeEvent{ 564 { 565 sourceASN: ciliumASN, 566 prefix: "10.100.1.1", 567 prefixLen: 32, 568 isWithdrawn: true, 569 }, 570 { 571 sourceASN: ciliumASN, 572 prefix: "10.200.1.1", 573 prefixLen: 32, 574 isWithdrawn: false, 575 }, 576 }, 577 }, 578 { 579 description: "advertise v6 service IP", 580 srvName: "service-b", 581 ingressIP: "cccc::1", 582 op: "add", 583 expectedRouteEvents: []routeEvent{ 584 { 585 sourceASN: ciliumASN, 586 prefix: "cccc::1", 587 prefixLen: 128, 588 isWithdrawn: false, 589 }, 590 }, 591 }, 592 { 593 description: "withdraw v6 service IP", 594 srvName: "service-b", 595 ingressIP: "", 596 op: "update", 597 expectedRouteEvents: []routeEvent{ 598 { 599 sourceASN: ciliumASN, 600 prefix: "cccc::1", 601 prefixLen: 128, 602 isWithdrawn: true, 603 }, 604 }, 605 }, 606 { 607 description: "re-advertise v6 service IP", 608 srvName: "service-b", 609 ingressIP: "cccc::1", 610 op: "update", 611 expectedRouteEvents: []routeEvent{ 612 { 613 sourceASN: ciliumASN, 614 prefix: "cccc::1", 615 prefixLen: 128, 616 isWithdrawn: false, 617 }, 618 }, 619 }, 620 { 621 description: "update v6 service IP", 622 srvName: "service-b", 623 ingressIP: "dddd::1", 624 op: "update", 625 expectedRouteEvents: []routeEvent{ 626 { 627 sourceASN: ciliumASN, 628 prefix: "cccc::1", 629 prefixLen: 128, 630 isWithdrawn: true, 631 }, 632 { 633 sourceASN: ciliumASN, 634 prefix: "dddd::1", 635 prefixLen: 128, 636 isWithdrawn: false, 637 }, 638 }, 639 }, 640 } 641 642 testCtx, testDone := context.WithTimeout(context.Background(), maxTestDuration) 643 defer testDone() 644 645 // setup topology 646 gobgpPeers, fixture, cleanup, err := setup(testCtx, t, []gobgpConfig{gobgpConf}, newFixtureConf()) 647 require.NoError(t, err) 648 require.Len(t, gobgpPeers, 1) 649 defer cleanup() 650 651 // setup neighbor 652 err = setupSingleNeighbor(testCtx, fixture, gobgpASN) 653 require.NoError(t, err) 654 655 // wait for peering to come up 656 err = gobgpPeers[0].waitForSessionState(testCtx, []string{"ESTABLISHED"}) 657 require.NoError(t, err) 658 659 // setup bgp policy with service selection 660 fixture.config.policy.Spec.VirtualRouters[0].ServiceSelector = &slim_metav1.LabelSelector{ 661 MatchExpressions: []slim_metav1.LabelSelectorRequirement{ 662 // always true match 663 { 664 Key: "somekey", 665 Operator: "NotIn", 666 Values: []string{"not-somekey"}, 667 }, 668 }, 669 } 670 fixture.config.policy.Spec.VirtualRouters[0].ServiceAdvertisements = []v2alpha1.BGPServiceAddressType{ 671 v2alpha1.BGPLoadBalancerIPAddr, 672 } 673 _, err = fixture.policyClient.Update(testCtx, &fixture.config.policy, meta_v1.UpdateOptions{}) 674 require.NoError(t, err) 675 676 tracker := fixture.fakeClientSet.SlimFakeClientset.Tracker() 677 678 for _, step := range steps { 679 t.Run(step.description, func(t *testing.T) { 680 srvObj := newLBServiceObj(lbSrvConfig{ 681 name: step.srvName, 682 ingressIP: step.ingressIP, 683 }) 684 685 if step.op == "add" { 686 err = tracker.Add(&srvObj) 687 } else { 688 err = tracker.Update(slim_metav1.Unversioned.WithResource("services"), &srvObj, "") 689 } 690 require.NoError(t, err, step.description) 691 692 // validate expected result 693 receivedEvents, err := gobgpPeers[0].getRouteEvents(testCtx, len(step.expectedRouteEvents)) 694 require.NoError(t, err, step.description) 695 696 // match events in any order 697 require.ElementsMatch(t, step.expectedRouteEvents, receivedEvents, step.description) 698 }) 699 } 700 } 701 702 // Test_LBEgressAdvertisementWithClusterIP validates Service v4 and v6 IPs is advertised, withdrawn and modified on changing policy. 703 func Test_LBEgressAdvertisementWithClusterIP(t *testing.T) { 704 testutils.PrivilegedTest(t) 705 706 var steps = []struct { 707 description string 708 srvName string 709 clusterIP string 710 op string // add or update 711 expectedRouteEvents []routeEvent 712 }{ 713 { 714 description: "advertise service IP", 715 srvName: "service-a", 716 clusterIP: "10.100.1.1", 717 op: "add", 718 expectedRouteEvents: []routeEvent{ 719 { 720 sourceASN: ciliumASN, 721 prefix: "10.100.1.1", 722 prefixLen: 32, 723 isWithdrawn: false, 724 }, 725 }, 726 }, 727 { 728 description: "withdraw service IP", 729 srvName: "service-a", 730 clusterIP: "", 731 op: "update", 732 expectedRouteEvents: []routeEvent{ 733 { 734 sourceASN: ciliumASN, 735 prefix: "10.100.1.1", 736 prefixLen: 32, 737 isWithdrawn: true, 738 }, 739 }, 740 }, 741 { 742 description: "re-advertise service IP", 743 srvName: "service-a", 744 clusterIP: "10.100.1.1", 745 op: "update", 746 expectedRouteEvents: []routeEvent{ 747 { 748 sourceASN: ciliumASN, 749 prefix: "10.100.1.1", 750 prefixLen: 32, 751 isWithdrawn: false, 752 }, 753 }, 754 }, 755 { 756 description: "update service IP", 757 srvName: "service-a", 758 clusterIP: "10.200.1.1", 759 op: "update", 760 expectedRouteEvents: []routeEvent{ 761 { 762 sourceASN: ciliumASN, 763 prefix: "10.100.1.1", 764 prefixLen: 32, 765 isWithdrawn: true, 766 }, 767 { 768 sourceASN: ciliumASN, 769 prefix: "10.200.1.1", 770 prefixLen: 32, 771 isWithdrawn: false, 772 }, 773 }, 774 }, 775 { 776 description: "advertise v6 service IP", 777 srvName: "service-b", 778 clusterIP: "cccc::1", 779 op: "add", 780 expectedRouteEvents: []routeEvent{ 781 { 782 sourceASN: ciliumASN, 783 prefix: "cccc::1", 784 prefixLen: 128, 785 isWithdrawn: false, 786 }, 787 }, 788 }, 789 { 790 description: "withdraw v6 service IP", 791 srvName: "service-b", 792 clusterIP: "", 793 op: "update", 794 expectedRouteEvents: []routeEvent{ 795 { 796 sourceASN: ciliumASN, 797 prefix: "cccc::1", 798 prefixLen: 128, 799 isWithdrawn: true, 800 }, 801 }, 802 }, 803 { 804 description: "re-advertise v6 service IP", 805 srvName: "service-b", 806 clusterIP: "cccc::1", 807 op: "update", 808 expectedRouteEvents: []routeEvent{ 809 { 810 sourceASN: ciliumASN, 811 prefix: "cccc::1", 812 prefixLen: 128, 813 isWithdrawn: false, 814 }, 815 }, 816 }, 817 { 818 description: "update v6 service IP", 819 srvName: "service-b", 820 clusterIP: "dddd::1", 821 op: "update", 822 expectedRouteEvents: []routeEvent{ 823 { 824 sourceASN: ciliumASN, 825 prefix: "cccc::1", 826 prefixLen: 128, 827 isWithdrawn: true, 828 }, 829 { 830 sourceASN: ciliumASN, 831 prefix: "dddd::1", 832 prefixLen: 128, 833 isWithdrawn: false, 834 }, 835 }, 836 }, 837 } 838 839 testCtx, testDone := context.WithTimeout(context.Background(), maxTestDuration) 840 defer testDone() 841 842 // setup topology 843 gobgpPeers, fixture, cleanup, err := setup(testCtx, t, []gobgpConfig{gobgpConf}, newFixtureConf()) 844 require.NoError(t, err) 845 require.Len(t, gobgpPeers, 1) 846 defer cleanup() 847 848 // setup neighbor 849 err = setupSingleNeighbor(testCtx, fixture, gobgpASN) 850 require.NoError(t, err) 851 852 // wait for peering to come up 853 err = gobgpPeers[0].waitForSessionState(testCtx, []string{"ESTABLISHED"}) 854 require.NoError(t, err) 855 856 // setup bgp policy with service selection 857 fixture.config.policy.Spec.VirtualRouters[0].ServiceSelector = &slim_metav1.LabelSelector{ 858 MatchExpressions: []slim_metav1.LabelSelectorRequirement{ 859 // always true match 860 { 861 Key: "somekey", 862 Operator: "NotIn", 863 Values: []string{"not-somekey"}, 864 }, 865 }, 866 } 867 fixture.config.policy.Spec.VirtualRouters[0].ServiceAdvertisements = []v2alpha1.BGPServiceAddressType{ 868 v2alpha1.BGPClusterIPAddr, 869 } 870 _, err = fixture.policyClient.Update(testCtx, &fixture.config.policy, meta_v1.UpdateOptions{}) 871 require.NoError(t, err) 872 873 tracker := fixture.fakeClientSet.SlimFakeClientset.Tracker() 874 875 for _, step := range steps { 876 t.Run(step.description, func(t *testing.T) { 877 srvObj := newClusterIPServiceObj(clusterIPSrvConfig{ 878 name: step.srvName, 879 clusterIP: step.clusterIP, 880 }) 881 882 if step.op == "add" { 883 err = tracker.Add(&srvObj) 884 } else { 885 err = tracker.Update(slim_metav1.Unversioned.WithResource("services"), &srvObj, "") 886 } 887 require.NoError(t, err, step.description) 888 889 // validate expected result 890 receivedEvents, err := gobgpPeers[0].getRouteEvents(testCtx, len(step.expectedRouteEvents)) 891 require.NoError(t, err, step.description) 892 893 // match events in any order 894 require.ElementsMatch(t, step.expectedRouteEvents, receivedEvents, step.description) 895 }) 896 } 897 } 898 899 // Test_LBEgressAdvertisementWithExternalIP validates Service v4 and v6 IPs is advertised, withdrawn and modified on changing policy. 900 func Test_LBEgressAdvertisementWithExternalIP(t *testing.T) { 901 testutils.PrivilegedTest(t) 902 903 var steps = []struct { 904 description string 905 srvName string 906 externalIP string 907 op string // add or update 908 expectedRouteEvents []routeEvent 909 }{ 910 { 911 description: "advertise service IP", 912 srvName: "service-a", 913 externalIP: "10.100.1.1", 914 op: "add", 915 expectedRouteEvents: []routeEvent{ 916 { 917 sourceASN: ciliumASN, 918 prefix: "10.100.1.1", 919 prefixLen: 32, 920 isWithdrawn: false, 921 }, 922 }, 923 }, 924 { 925 description: "withdraw service IP", 926 srvName: "service-a", 927 externalIP: "", 928 op: "update", 929 expectedRouteEvents: []routeEvent{ 930 { 931 sourceASN: ciliumASN, 932 prefix: "10.100.1.1", 933 prefixLen: 32, 934 isWithdrawn: true, 935 }, 936 }, 937 }, 938 { 939 description: "re-advertise service IP", 940 srvName: "service-a", 941 externalIP: "10.100.1.1", 942 op: "update", 943 expectedRouteEvents: []routeEvent{ 944 { 945 sourceASN: ciliumASN, 946 prefix: "10.100.1.1", 947 prefixLen: 32, 948 isWithdrawn: false, 949 }, 950 }, 951 }, 952 { 953 description: "update service IP", 954 srvName: "service-a", 955 externalIP: "10.200.1.1", 956 op: "update", 957 expectedRouteEvents: []routeEvent{ 958 { 959 sourceASN: ciliumASN, 960 prefix: "10.100.1.1", 961 prefixLen: 32, 962 isWithdrawn: true, 963 }, 964 { 965 sourceASN: ciliumASN, 966 prefix: "10.200.1.1", 967 prefixLen: 32, 968 isWithdrawn: false, 969 }, 970 }, 971 }, 972 { 973 description: "advertise v6 service IP", 974 srvName: "service-b", 975 externalIP: "cccc::1", 976 op: "add", 977 expectedRouteEvents: []routeEvent{ 978 { 979 sourceASN: ciliumASN, 980 prefix: "cccc::1", 981 prefixLen: 128, 982 isWithdrawn: false, 983 }, 984 }, 985 }, 986 { 987 description: "withdraw v6 service IP", 988 srvName: "service-b", 989 externalIP: "", 990 op: "update", 991 expectedRouteEvents: []routeEvent{ 992 { 993 sourceASN: ciliumASN, 994 prefix: "cccc::1", 995 prefixLen: 128, 996 isWithdrawn: true, 997 }, 998 }, 999 }, 1000 { 1001 description: "re-advertise v6 service IP", 1002 srvName: "service-b", 1003 externalIP: "cccc::1", 1004 op: "update", 1005 expectedRouteEvents: []routeEvent{ 1006 { 1007 sourceASN: ciliumASN, 1008 prefix: "cccc::1", 1009 prefixLen: 128, 1010 isWithdrawn: false, 1011 }, 1012 }, 1013 }, 1014 { 1015 description: "update v6 service IP", 1016 srvName: "service-b", 1017 externalIP: "dddd::1", 1018 op: "update", 1019 expectedRouteEvents: []routeEvent{ 1020 { 1021 sourceASN: ciliumASN, 1022 prefix: "cccc::1", 1023 prefixLen: 128, 1024 isWithdrawn: true, 1025 }, 1026 { 1027 sourceASN: ciliumASN, 1028 prefix: "dddd::1", 1029 prefixLen: 128, 1030 isWithdrawn: false, 1031 }, 1032 }, 1033 }, 1034 } 1035 1036 testCtx, testDone := context.WithTimeout(context.Background(), maxTestDuration) 1037 defer testDone() 1038 1039 // setup topology 1040 gobgpPeers, fixture, cleanup, err := setup(testCtx, t, []gobgpConfig{gobgpConf}, newFixtureConf()) 1041 require.NoError(t, err) 1042 require.Len(t, gobgpPeers, 1) 1043 defer cleanup() 1044 1045 // setup neighbor 1046 err = setupSingleNeighbor(testCtx, fixture, gobgpASN) 1047 require.NoError(t, err) 1048 1049 // wait for peering to come up 1050 err = gobgpPeers[0].waitForSessionState(testCtx, []string{"ESTABLISHED"}) 1051 require.NoError(t, err) 1052 1053 // setup bgp policy with service selection 1054 fixture.config.policy.Spec.VirtualRouters[0].ServiceSelector = &slim_metav1.LabelSelector{ 1055 MatchExpressions: []slim_metav1.LabelSelectorRequirement{ 1056 // always true match 1057 { 1058 Key: "somekey", 1059 Operator: "NotIn", 1060 Values: []string{"not-somekey"}, 1061 }, 1062 }, 1063 } 1064 fixture.config.policy.Spec.VirtualRouters[0].ServiceAdvertisements = []v2alpha1.BGPServiceAddressType{ 1065 v2alpha1.BGPExternalIPAddr, 1066 } 1067 _, err = fixture.policyClient.Update(testCtx, &fixture.config.policy, meta_v1.UpdateOptions{}) 1068 require.NoError(t, err) 1069 1070 tracker := fixture.fakeClientSet.SlimFakeClientset.Tracker() 1071 1072 for _, step := range steps { 1073 t.Run(step.description, func(t *testing.T) { 1074 srvObj := newExternalIPServiceObj(externalIPsSrvConfig{ 1075 name: step.srvName, 1076 externalIP: step.externalIP, 1077 }) 1078 1079 if step.op == "add" { 1080 err = tracker.Add(&srvObj) 1081 } else { 1082 err = tracker.Update(slim_metav1.Unversioned.WithResource("services"), &srvObj, "") 1083 } 1084 require.NoError(t, err, step.description) 1085 1086 // validate expected result 1087 receivedEvents, err := gobgpPeers[0].getRouteEvents(testCtx, len(step.expectedRouteEvents)) 1088 require.NoError(t, err, step.description) 1089 1090 // match events in any order 1091 require.ElementsMatch(t, step.expectedRouteEvents, receivedEvents, step.description) 1092 }) 1093 } 1094 } 1095 1096 // Test_AdvertisedPathAttributes validates optional path attributes in advertised paths. 1097 func Test_AdvertisedPathAttributes(t *testing.T) { 1098 testutils.PrivilegedTest(t) 1099 1100 var steps = []struct { 1101 description string 1102 op string // add or update 1103 podCIDRs []string 1104 lbService *lbSrvConfig 1105 lbPool *lbPoolConfig 1106 advertiseAttributes []v2alpha1.CiliumBGPPathAttributes 1107 expectedRouteEvent routeEvent 1108 }{ 1109 { 1110 description: "advertise pod CIDR with standard community + non-default local pref", 1111 op: "add", 1112 podCIDRs: []string{"10.1.0.0/16"}, 1113 advertiseAttributes: []v2alpha1.CiliumBGPPathAttributes{ 1114 { 1115 SelectorType: v2alpha1.PodCIDRSelectorName, 1116 Communities: &v2alpha1.BGPCommunities{ 1117 Standard: []v2alpha1.BGPStandardCommunity{v2alpha1.BGPStandardCommunity("64125:100")}, 1118 }, 1119 LocalPreference: pointer.Int64(150), 1120 }, 1121 }, 1122 expectedRouteEvent: routeEvent{ 1123 sourceASN: ciliumASN, 1124 prefix: "10.1.0.0", 1125 prefixLen: 16, 1126 isWithdrawn: false, 1127 extraPathAttributes: []bgp.PathAttributeInterface{ 1128 bgp.NewPathAttributeLocalPref(150), 1129 bgp.NewPathAttributeCommunities([]uint32{parseCommunity("64125:100")}), 1130 }, 1131 }, 1132 }, 1133 { 1134 description: "advertise service IP with large community", 1135 op: "add", 1136 lbService: &lbSrvConfig{ 1137 name: "service-a", 1138 ingressIP: "10.100.1.111", 1139 }, 1140 lbPool: &lbPoolConfig{ 1141 name: "pool-a", 1142 cidrs: []string{"10.100.1.0/24"}, 1143 }, 1144 advertiseAttributes: []v2alpha1.CiliumBGPPathAttributes{ 1145 { 1146 SelectorType: v2alpha1.CiliumLoadBalancerIPPoolSelectorName, 1147 Communities: &v2alpha1.BGPCommunities{ 1148 Large: []v2alpha1.BGPLargeCommunity{v2alpha1.BGPLargeCommunity("64125:100:200")}, 1149 }, 1150 }, 1151 }, 1152 expectedRouteEvent: routeEvent{ 1153 sourceASN: ciliumASN, 1154 prefix: "10.100.1.111", 1155 prefixLen: 32, 1156 isWithdrawn: false, 1157 extraPathAttributes: []bgp.PathAttributeInterface{ 1158 bgp.NewPathAttributeLocalPref(100), 1159 bgp.NewPathAttributeLargeCommunities([]*bgp.LargeCommunity{ 1160 { 1161 ASN: 64125, 1162 LocalData1: 100, 1163 LocalData2: 200, 1164 }, 1165 }), 1166 }, 1167 }, 1168 }, 1169 { 1170 description: "advertise service IP with well-known community", 1171 op: "add", 1172 lbService: &lbSrvConfig{ 1173 name: "service-b", 1174 ingressIP: "10.100.1.112", 1175 }, 1176 advertiseAttributes: []v2alpha1.CiliumBGPPathAttributes{ 1177 { 1178 SelectorType: v2alpha1.CiliumLoadBalancerIPPoolSelectorName, 1179 Communities: &v2alpha1.BGPCommunities{ 1180 WellKnown: []v2alpha1.BGPWellKnownCommunity{v2alpha1.BGPWellKnownCommunity("no-export")}, 1181 }, 1182 }, 1183 }, 1184 expectedRouteEvent: routeEvent{ 1185 sourceASN: ciliumASN, 1186 prefix: "10.100.1.112", 1187 prefixLen: 32, 1188 isWithdrawn: false, 1189 extraPathAttributes: []bgp.PathAttributeInterface{ 1190 bgp.NewPathAttributeLocalPref(100), 1191 bgp.NewPathAttributeCommunities([]uint32{parseCommunity("65535:65281")}), // = no-export 1192 }, 1193 }, 1194 }, 1195 { 1196 description: "advertise service IP with duplicate well-known community", 1197 op: "add", 1198 lbService: &lbSrvConfig{ 1199 name: "service-c", 1200 ingressIP: "10.100.1.113", 1201 }, 1202 advertiseAttributes: []v2alpha1.CiliumBGPPathAttributes{ 1203 { 1204 SelectorType: v2alpha1.CiliumLoadBalancerIPPoolSelectorName, 1205 Communities: &v2alpha1.BGPCommunities{ 1206 Standard: []v2alpha1.BGPStandardCommunity{v2alpha1.BGPStandardCommunity("65535:65281")}, // = no-export 1207 WellKnown: []v2alpha1.BGPWellKnownCommunity{v2alpha1.BGPWellKnownCommunity("no-export")}, 1208 }, 1209 }, 1210 }, 1211 expectedRouteEvent: routeEvent{ 1212 sourceASN: ciliumASN, 1213 prefix: "10.100.1.113", 1214 prefixLen: 32, 1215 isWithdrawn: false, 1216 extraPathAttributes: []bgp.PathAttributeInterface{ 1217 bgp.NewPathAttributeLocalPref(100), 1218 bgp.NewPathAttributeCommunities([]uint32{parseCommunity("65535:65281")}), // = no-export 1219 }, 1220 }, 1221 }, 1222 } 1223 1224 testCtx, testDone := context.WithTimeout(context.Background(), maxTestDuration) 1225 defer testDone() 1226 1227 // setup topology - iBGP (ASN == ciliumASN) 1228 gobgpPeers, fixture, cleanup, err := setup(testCtx, t, []gobgpConfig{gobgpConfIBGP}, newFixtureConf()) 1229 require.NoError(t, err) 1230 require.Len(t, gobgpPeers, 1) 1231 defer cleanup() 1232 1233 // setup neighbor - iBGP (ASN == ciliumASN) 1234 err = setupSingleNeighbor(testCtx, fixture, ciliumASN) 1235 require.NoError(t, err) 1236 1237 // wait for peering to come up 1238 err = gobgpPeers[0].waitForSessionState(testCtx, []string{"ESTABLISHED"}) 1239 require.NoError(t, err) 1240 1241 // setup bgp policy with service selection 1242 fixture.config.policy.Spec.VirtualRouters[0].ServiceSelector = &slim_metav1.LabelSelector{ 1243 MatchExpressions: []slim_metav1.LabelSelectorRequirement{ 1244 // always true match 1245 { 1246 Key: "somekey", 1247 Operator: "NotIn", 1248 Values: []string{"not-somekey"}, 1249 }, 1250 }, 1251 } 1252 _, err = fixture.policyClient.Update(testCtx, &fixture.config.policy, meta_v1.UpdateOptions{}) 1253 require.NoError(t, err) 1254 1255 slimTracker := fixture.fakeClientSet.SlimFakeClientset.Tracker() 1256 ciliumTracker := fixture.fakeClientSet.CiliumFakeClientset.Tracker() 1257 obj, err := ciliumTracker.Get(v2.SchemeGroupVersion.WithResource("ciliumnodes"), "", baseNode.name) 1258 require.NoError(t, err) 1259 1260 node, ok := obj.(*v2.CiliumNode) 1261 require.True(t, ok) 1262 1263 for _, step := range steps { 1264 t.Run(step.description, func(t *testing.T) { 1265 // setup advertised path attributes 1266 fixture.config.policy.Spec.VirtualRouters[0].Neighbors[0].AdvertisedPathAttributes = step.advertiseAttributes 1267 1268 _, err = fixture.policyClient.Update(testCtx, &fixture.config.policy, meta_v1.UpdateOptions{}) 1269 require.NoError(t, err) 1270 1271 if step.podCIDRs != nil { 1272 // update CiliumNode with new PodCIDR 1273 node.Spec.IPAM.PodCIDRs = step.podCIDRs 1274 err = ciliumTracker.Update(v2.SchemeGroupVersion.WithResource("ciliumnodes"), node, "") 1275 require.NoError(t, err) 1276 } 1277 1278 if step.lbPool != nil { 1279 // add / update LB IP pool 1280 lbPoolObj := newLBPoolObj(*step.lbPool) 1281 if step.op == "add" { 1282 err = ciliumTracker.Add(&lbPoolObj) 1283 } else { 1284 err = ciliumTracker.Update(v2alpha1.SchemeGroupVersion.WithResource("ciliumloadbalancerippool"), &lbPoolObj, "") 1285 } 1286 require.NoError(t, err, step.description) 1287 } 1288 1289 if step.lbService != nil { 1290 // add / update LB service 1291 srvObj := newLBServiceObj(*step.lbService) 1292 if step.op == "add" { 1293 err = slimTracker.Add(&srvObj) 1294 } else { 1295 err = slimTracker.Update(slim_metav1.Unversioned.WithResource("services"), &srvObj, "") 1296 } 1297 require.NoError(t, err, step.description) 1298 } 1299 1300 receivedRouteMatch := func() bool { 1301 // validate received vs. expected route event 1302 receivedEvents, err := gobgpPeers[0].getRouteEvents(testCtx, 1) 1303 require.NoError(t, err, step.description) 1304 equal := reflect.DeepEqual(step.expectedRouteEvent, receivedEvents[0]) 1305 if !equal { 1306 t.Logf("route events not (yet) equal - expected: %v, actual: %v", step.expectedRouteEvent, receivedEvents[0]) 1307 } 1308 return equal 1309 } 1310 1311 deadline, _ := testCtx.Deadline() 1312 outstanding := time.Until(deadline) 1313 require.Greater(t, outstanding, 0*time.Second, "test context deadline exceeded") 1314 1315 // Retry receivedRouteMatch once per second until the test context deadline. 1316 // We may need to retry as the received route does not need to match the expected route immediately, 1317 // we may receive a route without expected path attributes before the necessary route policy is in place. 1318 require.Eventually(t, receivedRouteMatch, outstanding, 100*time.Millisecond) 1319 }) 1320 } 1321 } 1322 1323 func parseCommunity(c string) uint32 { 1324 elems := strings.Split(c, ":") 1325 if len(elems) < 2 { 1326 return 0 1327 } 1328 fst, _ := strconv.ParseUint(elems[0], 10, 16) 1329 snd, _ := strconv.ParseUint(elems[1], 10, 16) 1330 return uint32(fst<<16 | snd) 1331 }