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  }