github.com/cilium/cilium@v1.16.2/pkg/bgpv1/manager/reconciler/route_policy_test.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package reconciler
     5  
     6  import (
     7  	"context"
     8  	"net/netip"
     9  	"testing"
    10  
    11  	"github.com/stretchr/testify/require"
    12  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    13  	"k8s.io/utils/ptr"
    14  
    15  	"github.com/cilium/cilium/pkg/bgpv1/manager/instance"
    16  	"github.com/cilium/cilium/pkg/bgpv1/manager/store"
    17  	"github.com/cilium/cilium/pkg/bgpv1/types"
    18  	ipamTypes "github.com/cilium/cilium/pkg/ipam/types"
    19  	v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
    20  	v2alpha1api "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1"
    21  	slimv1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1"
    22  )
    23  
    24  var (
    25  	podCIDR       = "10.0.0.0/24"
    26  	podCIDRPrefix = netip.MustParsePrefix(podCIDR)
    27  
    28  	lbPool = &v2alpha1api.CiliumLoadBalancerIPPool{
    29  		ObjectMeta: metav1.ObjectMeta{
    30  			Labels: map[string]string{
    31  				"label1": "value1",
    32  			},
    33  		},
    34  		Spec: v2alpha1api.CiliumLoadBalancerIPPoolSpec{
    35  			Blocks: []v2alpha1api.CiliumLoadBalancerIPPoolIPBlock{
    36  				{
    37  					Cidr: "192.168.0.0/24",
    38  				},
    39  			},
    40  		},
    41  	}
    42  
    43  	lbPoolUpdated = &v2alpha1api.CiliumLoadBalancerIPPool{
    44  		ObjectMeta: metav1.ObjectMeta{
    45  			Labels: map[string]string{
    46  				"label1": "value1",
    47  			},
    48  		},
    49  		Spec: v2alpha1api.CiliumLoadBalancerIPPoolSpec{
    50  			Blocks: []v2alpha1api.CiliumLoadBalancerIPPoolIPBlock{
    51  				{
    52  					Cidr: "10.100.99.0/24", // UPDATED
    53  				},
    54  			},
    55  		},
    56  	}
    57  
    58  	podPool = &v2alpha1api.CiliumPodIPPool{
    59  		ObjectMeta: metav1.ObjectMeta{
    60  			Name: "test",
    61  			Labels: map[string]string{
    62  				"label1": "value1",
    63  			}},
    64  		Spec: v2alpha1api.IPPoolSpec{
    65  			IPv4: &v2alpha1api.IPv4PoolSpec{
    66  				CIDRs:    []v2alpha1api.PoolCIDR{"100.0.0.0/16"},
    67  				MaskSize: 24,
    68  			},
    69  			IPv6: &v2alpha1api.IPv6PoolSpec{
    70  				CIDRs:    []v2alpha1api.PoolCIDR{"2001:0:0:1234::/64"},
    71  				MaskSize: 96,
    72  			},
    73  		},
    74  	}
    75  
    76  	podPoolUpdated = &v2alpha1api.CiliumPodIPPool{
    77  		ObjectMeta: metav1.ObjectMeta{
    78  			Labels: map[string]string{
    79  				"label1": "value1",
    80  			}},
    81  		Spec: v2alpha1api.IPPoolSpec{
    82  			IPv4: &v2alpha1api.IPv4PoolSpec{
    83  				CIDRs:    []v2alpha1api.PoolCIDR{"100.0.0.0/16", "100.1.0.0/16"},
    84  				MaskSize: 24,
    85  			},
    86  			IPv6: &v2alpha1api.IPv6PoolSpec{
    87  				CIDRs:    []v2alpha1api.PoolCIDR{"2001:0:0:1234::/64", "2002:0:0:1234::/64"},
    88  				MaskSize: 96,
    89  			},
    90  		},
    91  	}
    92  
    93  	nodePool = ipamTypes.IPAMPoolAllocation{
    94  		Pool: podPool.Name,
    95  		CIDRs: []ipamTypes.IPAMPodCIDR{
    96  			"100.0.0.0/16",
    97  			"2001:0:0:1234::/64",
    98  		},
    99  	}
   100  
   101  	nodePoolUpdated = ipamTypes.IPAMPoolAllocation{
   102  		Pool: podPool.Name,
   103  		CIDRs: []ipamTypes.IPAMPodCIDR{
   104  			"100.0.0.0/16",
   105  			"100.1.0.0/16",
   106  			"2001:0:0:1234::/64",
   107  			"2002:0:0:1234::/64",
   108  		},
   109  	}
   110  
   111  	peerAddress = "172.16.0.1/32"
   112  
   113  	standardCommunity  = "64125:100"
   114  	wellKnownCommunity = "no-advertise"
   115  	largeCommunity     = "64125:4294967295:100"
   116  
   117  	attrSelectLBPool = v2alpha1api.CiliumBGPPathAttributes{
   118  		SelectorType: v2alpha1api.CiliumLoadBalancerIPPoolSelectorName,
   119  		Selector: &slimv1.LabelSelector{
   120  			MatchLabels: map[string]slimv1.MatchLabelsValue{
   121  				"label1": "value1",
   122  			},
   123  		},
   124  		Communities: &v2alpha1api.BGPCommunities{
   125  			Standard:  []v2alpha1api.BGPStandardCommunity{v2alpha1api.BGPStandardCommunity(standardCommunity)},
   126  			WellKnown: []v2alpha1api.BGPWellKnownCommunity{v2alpha1api.BGPWellKnownCommunity(wellKnownCommunity)},
   127  			Large:     []v2alpha1api.BGPLargeCommunity{v2alpha1api.BGPLargeCommunity(largeCommunity)},
   128  		},
   129  	}
   130  
   131  	attrSelectPodPool = v2alpha1api.CiliumBGPPathAttributes{
   132  		SelectorType: v2alpha1api.CiliumPodIPPoolSelectorName,
   133  		Selector: &slimv1.LabelSelector{
   134  			MatchLabels: map[string]slimv1.MatchLabelsValue{
   135  				"label1": "value1",
   136  			},
   137  		},
   138  		Communities: &v2alpha1api.BGPCommunities{
   139  			Standard:  []v2alpha1api.BGPStandardCommunity{v2alpha1api.BGPStandardCommunity(standardCommunity)},
   140  			WellKnown: []v2alpha1api.BGPWellKnownCommunity{v2alpha1api.BGPWellKnownCommunity(wellKnownCommunity)},
   141  			Large:     []v2alpha1api.BGPLargeCommunity{v2alpha1api.BGPLargeCommunity(largeCommunity)},
   142  		},
   143  	}
   144  
   145  	attrSelectAnyNode = v2alpha1api.CiliumBGPPathAttributes{
   146  		SelectorType:    v2alpha1api.PodCIDRSelectorName,
   147  		LocalPreference: ptr.To[int64](150),
   148  	}
   149  
   150  	attrSelectNonExistingNode = v2alpha1api.CiliumBGPPathAttributes{
   151  		SelectorType: v2alpha1api.PodCIDRSelectorName,
   152  		Selector: &slimv1.LabelSelector{
   153  			MatchLabels: map[string]slimv1.MatchLabelsValue{
   154  				"node": "non-existing",
   155  			},
   156  		},
   157  		LocalPreference: ptr.To[int64](150),
   158  	}
   159  
   160  	attrSelectInvalid = v2alpha1api.CiliumBGPPathAttributes{
   161  		SelectorType: "INVALID",
   162  		Selector: &slimv1.LabelSelector{
   163  			MatchLabels: map[string]slimv1.MatchLabelsValue{
   164  				"env": "dev",
   165  			},
   166  		},
   167  	}
   168  )
   169  
   170  type routePolicyTestInputs struct {
   171  	podCIDRs         []string
   172  	LBPools          []*v2alpha1api.CiliumLoadBalancerIPPool
   173  	NodePools        []ipamTypes.IPAMPoolAllocation
   174  	PodPools         []*v2alpha1api.CiliumPodIPPool
   175  	neighbors        []v2alpha1api.CiliumBGPNeighbor
   176  	expectedPolicies []*types.RoutePolicy
   177  }
   178  
   179  func TestRoutePolicyReconciler(t *testing.T) {
   180  	var table = []struct {
   181  		name        string
   182  		initial     *routePolicyTestInputs
   183  		updated     *routePolicyTestInputs
   184  		expectError bool
   185  	}{
   186  		{
   187  			name: "add complex policy (pod CIDR + LB pool + Pod pool)",
   188  			initial: &routePolicyTestInputs{
   189  				podCIDRs: []string{
   190  					podCIDR,
   191  				},
   192  				LBPools: []*v2alpha1api.CiliumLoadBalancerIPPool{
   193  					lbPool,
   194  				},
   195  				PodPools: []*v2alpha1api.CiliumPodIPPool{
   196  					podPool,
   197  				},
   198  				NodePools: []ipamTypes.IPAMPoolAllocation{
   199  					nodePool,
   200  				},
   201  				neighbors: []v2alpha1api.CiliumBGPNeighbor{
   202  					{
   203  						PeerAddress: peerAddress,
   204  						AdvertisedPathAttributes: []v2alpha1api.CiliumBGPPathAttributes{
   205  							attrSelectLBPool,
   206  							attrSelectPodPool,
   207  							attrSelectAnyNode,
   208  						},
   209  					},
   210  				},
   211  				expectedPolicies: []*types.RoutePolicy{
   212  					{
   213  						Name: pathAttributesPolicyName(attrSelectLBPool, peerAddress),
   214  						Type: types.RoutePolicyTypeExport,
   215  						Statements: []*types.RoutePolicyStatement{
   216  							{
   217  								Conditions: types.RoutePolicyConditions{
   218  									MatchNeighbors: []string{peerAddress},
   219  									MatchPrefixes: []*types.RoutePolicyPrefixMatch{
   220  										{
   221  											CIDR:         netip.MustParsePrefix(string(lbPool.Spec.Blocks[0].Cidr)),
   222  											PrefixLenMin: maxPrefixLenIPv4,
   223  											PrefixLenMax: maxPrefixLenIPv4,
   224  										},
   225  									},
   226  								},
   227  								Actions: types.RoutePolicyActions{
   228  									RouteAction:         types.RoutePolicyActionNone,
   229  									AddCommunities:      []string{standardCommunity, wellKnownCommunity},
   230  									AddLargeCommunities: []string{largeCommunity},
   231  								},
   232  							},
   233  						},
   234  					},
   235  					{
   236  						Name: pathAttributesPolicyName(attrSelectPodPool, peerAddress),
   237  						Type: types.RoutePolicyTypeExport,
   238  						Statements: []*types.RoutePolicyStatement{
   239  							{
   240  								Conditions: types.RoutePolicyConditions{
   241  									MatchNeighbors: []string{peerAddress},
   242  									MatchPrefixes: []*types.RoutePolicyPrefixMatch{
   243  										{
   244  											CIDR:         netip.MustParsePrefix(string(podPool.Spec.IPv4.CIDRs[0])),
   245  											PrefixLenMin: int(podPool.Spec.IPv4.MaskSize),
   246  											PrefixLenMax: int(podPool.Spec.IPv4.MaskSize),
   247  										},
   248  									},
   249  								},
   250  								Actions: types.RoutePolicyActions{
   251  									RouteAction:         types.RoutePolicyActionNone,
   252  									AddCommunities:      []string{standardCommunity, wellKnownCommunity},
   253  									AddLargeCommunities: []string{largeCommunity},
   254  								},
   255  							},
   256  							{
   257  								Conditions: types.RoutePolicyConditions{
   258  									MatchNeighbors: []string{peerAddress},
   259  									MatchPrefixes: []*types.RoutePolicyPrefixMatch{
   260  										{
   261  											CIDR:         netip.MustParsePrefix(string(podPool.Spec.IPv6.CIDRs[0])),
   262  											PrefixLenMin: int(podPool.Spec.IPv6.MaskSize),
   263  											PrefixLenMax: int(podPool.Spec.IPv6.MaskSize),
   264  										},
   265  									},
   266  								},
   267  								Actions: types.RoutePolicyActions{
   268  									RouteAction:         types.RoutePolicyActionNone,
   269  									AddCommunities:      []string{standardCommunity, wellKnownCommunity},
   270  									AddLargeCommunities: []string{largeCommunity},
   271  								},
   272  							},
   273  						},
   274  					},
   275  					{
   276  						Name: pathAttributesPolicyName(attrSelectAnyNode, peerAddress),
   277  						Type: types.RoutePolicyTypeExport,
   278  						Statements: []*types.RoutePolicyStatement{
   279  							{
   280  								Conditions: types.RoutePolicyConditions{
   281  									MatchNeighbors: []string{peerAddress},
   282  									MatchPrefixes: []*types.RoutePolicyPrefixMatch{
   283  										{
   284  											CIDR:         podCIDRPrefix,
   285  											PrefixLenMin: podCIDRPrefix.Bits(),
   286  											PrefixLenMax: podCIDRPrefix.Bits(),
   287  										},
   288  									},
   289  								},
   290  								Actions: types.RoutePolicyActions{
   291  									RouteAction:        types.RoutePolicyActionNone,
   292  									SetLocalPreference: attrSelectAnyNode.LocalPreference,
   293  								},
   294  							},
   295  						},
   296  					},
   297  				},
   298  			},
   299  			expectError: false,
   300  		},
   301  		{
   302  			name: "update policy - lb pool change",
   303  			initial: &routePolicyTestInputs{
   304  				LBPools: []*v2alpha1api.CiliumLoadBalancerIPPool{
   305  					lbPool,
   306  				},
   307  				neighbors: []v2alpha1api.CiliumBGPNeighbor{
   308  					{
   309  						PeerAddress: peerAddress,
   310  						AdvertisedPathAttributes: []v2alpha1api.CiliumBGPPathAttributes{
   311  							attrSelectLBPool,
   312  						},
   313  					},
   314  				},
   315  				expectedPolicies: []*types.RoutePolicy{
   316  					{
   317  						Name: pathAttributesPolicyName(attrSelectLBPool, peerAddress),
   318  						Type: types.RoutePolicyTypeExport,
   319  						Statements: []*types.RoutePolicyStatement{
   320  							{
   321  								Conditions: types.RoutePolicyConditions{
   322  									MatchNeighbors: []string{peerAddress},
   323  									MatchPrefixes: []*types.RoutePolicyPrefixMatch{
   324  										{
   325  											CIDR:         netip.MustParsePrefix(string(lbPool.Spec.Blocks[0].Cidr)),
   326  											PrefixLenMin: maxPrefixLenIPv4,
   327  											PrefixLenMax: maxPrefixLenIPv4,
   328  										},
   329  									},
   330  								},
   331  								Actions: types.RoutePolicyActions{
   332  									RouteAction:         types.RoutePolicyActionNone,
   333  									AddCommunities:      []string{standardCommunity, wellKnownCommunity},
   334  									AddLargeCommunities: []string{largeCommunity},
   335  								},
   336  							},
   337  						},
   338  					},
   339  				},
   340  			},
   341  			updated: &routePolicyTestInputs{
   342  				LBPools: []*v2alpha1api.CiliumLoadBalancerIPPool{
   343  					lbPoolUpdated, // UPDATED - modified CIDR
   344  				},
   345  				neighbors: []v2alpha1api.CiliumBGPNeighbor{
   346  					{
   347  						PeerAddress: peerAddress,
   348  						AdvertisedPathAttributes: []v2alpha1api.CiliumBGPPathAttributes{
   349  							attrSelectLBPool,
   350  						},
   351  					},
   352  				},
   353  				expectedPolicies: []*types.RoutePolicy{
   354  					{
   355  						Name: pathAttributesPolicyName(attrSelectLBPool, peerAddress),
   356  						Type: types.RoutePolicyTypeExport,
   357  						Statements: []*types.RoutePolicyStatement{
   358  							{
   359  								Conditions: types.RoutePolicyConditions{
   360  									MatchNeighbors: []string{peerAddress},
   361  									MatchPrefixes: []*types.RoutePolicyPrefixMatch{
   362  										{
   363  											CIDR:         netip.MustParsePrefix(string(lbPoolUpdated.Spec.Blocks[0].Cidr)),
   364  											PrefixLenMin: maxPrefixLenIPv4,
   365  											PrefixLenMax: maxPrefixLenIPv4,
   366  										},
   367  									},
   368  								},
   369  								Actions: types.RoutePolicyActions{
   370  									RouteAction:         types.RoutePolicyActionNone,
   371  									AddCommunities:      []string{standardCommunity, wellKnownCommunity},
   372  									AddLargeCommunities: []string{largeCommunity},
   373  								},
   374  							},
   375  						},
   376  					},
   377  				},
   378  			},
   379  			expectError: false,
   380  		},
   381  		{
   382  			name: "update policy - pod pool change",
   383  			initial: &routePolicyTestInputs{
   384  				PodPools: []*v2alpha1api.CiliumPodIPPool{
   385  					podPool,
   386  				},
   387  				NodePools: []ipamTypes.IPAMPoolAllocation{
   388  					nodePool,
   389  				},
   390  				neighbors: []v2alpha1api.CiliumBGPNeighbor{
   391  					{
   392  						PeerAddress: peerAddress,
   393  						AdvertisedPathAttributes: []v2alpha1api.CiliumBGPPathAttributes{
   394  							attrSelectPodPool,
   395  						},
   396  					},
   397  				},
   398  				expectedPolicies: []*types.RoutePolicy{
   399  					{
   400  						Name: pathAttributesPolicyName(attrSelectPodPool, peerAddress),
   401  						Type: types.RoutePolicyTypeExport,
   402  						Statements: []*types.RoutePolicyStatement{
   403  							{
   404  								Conditions: types.RoutePolicyConditions{
   405  									MatchNeighbors: []string{peerAddress},
   406  									MatchPrefixes: []*types.RoutePolicyPrefixMatch{
   407  										{
   408  											CIDR:         netip.MustParsePrefix(string(podPool.Spec.IPv4.CIDRs[0])),
   409  											PrefixLenMin: int(podPool.Spec.IPv4.MaskSize),
   410  											PrefixLenMax: int(podPool.Spec.IPv4.MaskSize),
   411  										},
   412  									},
   413  								},
   414  								Actions: types.RoutePolicyActions{
   415  									RouteAction:         types.RoutePolicyActionNone,
   416  									AddCommunities:      []string{standardCommunity, wellKnownCommunity},
   417  									AddLargeCommunities: []string{largeCommunity},
   418  								},
   419  							},
   420  							{
   421  								Conditions: types.RoutePolicyConditions{
   422  									MatchNeighbors: []string{peerAddress},
   423  									MatchPrefixes: []*types.RoutePolicyPrefixMatch{
   424  										{
   425  											CIDR:         netip.MustParsePrefix(string(podPool.Spec.IPv6.CIDRs[0])),
   426  											PrefixLenMin: int(podPool.Spec.IPv6.MaskSize),
   427  											PrefixLenMax: int(podPool.Spec.IPv6.MaskSize),
   428  										},
   429  									},
   430  								},
   431  								Actions: types.RoutePolicyActions{
   432  									RouteAction:         types.RoutePolicyActionNone,
   433  									AddCommunities:      []string{standardCommunity, wellKnownCommunity},
   434  									AddLargeCommunities: []string{largeCommunity},
   435  								},
   436  							},
   437  						},
   438  					},
   439  				},
   440  			},
   441  			updated: &routePolicyTestInputs{
   442  				PodPools: []*v2alpha1api.CiliumPodIPPool{
   443  					podPoolUpdated,
   444  				},
   445  				NodePools: []ipamTypes.IPAMPoolAllocation{
   446  					nodePoolUpdated,
   447  				},
   448  				neighbors: []v2alpha1api.CiliumBGPNeighbor{
   449  					{
   450  						PeerAddress: peerAddress,
   451  						AdvertisedPathAttributes: []v2alpha1api.CiliumBGPPathAttributes{
   452  							attrSelectPodPool,
   453  						},
   454  					},
   455  				},
   456  				expectedPolicies: []*types.RoutePolicy{
   457  					{
   458  						Name: pathAttributesPolicyName(attrSelectPodPool, peerAddress),
   459  						Type: types.RoutePolicyTypeExport,
   460  						Statements: []*types.RoutePolicyStatement{
   461  							{
   462  								Conditions: types.RoutePolicyConditions{
   463  									MatchNeighbors: []string{peerAddress},
   464  									MatchPrefixes: []*types.RoutePolicyPrefixMatch{
   465  										{
   466  											CIDR:         netip.MustParsePrefix(string(podPoolUpdated.Spec.IPv4.CIDRs[0])),
   467  											PrefixLenMin: int(podPoolUpdated.Spec.IPv4.MaskSize),
   468  											PrefixLenMax: int(podPoolUpdated.Spec.IPv4.MaskSize),
   469  										},
   470  										{
   471  											CIDR:         netip.MustParsePrefix(string(podPoolUpdated.Spec.IPv4.CIDRs[1])),
   472  											PrefixLenMin: int(podPoolUpdated.Spec.IPv4.MaskSize),
   473  											PrefixLenMax: int(podPoolUpdated.Spec.IPv4.MaskSize),
   474  										},
   475  									},
   476  								},
   477  								Actions: types.RoutePolicyActions{
   478  									RouteAction:         types.RoutePolicyActionNone,
   479  									AddCommunities:      []string{standardCommunity, wellKnownCommunity},
   480  									AddLargeCommunities: []string{largeCommunity},
   481  								},
   482  							},
   483  							{
   484  								Conditions: types.RoutePolicyConditions{
   485  									MatchNeighbors: []string{peerAddress},
   486  									MatchPrefixes: []*types.RoutePolicyPrefixMatch{
   487  										{
   488  											CIDR:         netip.MustParsePrefix(string(podPoolUpdated.Spec.IPv6.CIDRs[0])),
   489  											PrefixLenMin: int(podPoolUpdated.Spec.IPv6.MaskSize),
   490  											PrefixLenMax: int(podPoolUpdated.Spec.IPv6.MaskSize),
   491  										},
   492  										{
   493  											CIDR:         netip.MustParsePrefix(string(podPoolUpdated.Spec.IPv6.CIDRs[1])),
   494  											PrefixLenMin: int(podPoolUpdated.Spec.IPv6.MaskSize),
   495  											PrefixLenMax: int(podPoolUpdated.Spec.IPv6.MaskSize),
   496  										},
   497  									},
   498  								},
   499  								Actions: types.RoutePolicyActions{
   500  									RouteAction:         types.RoutePolicyActionNone,
   501  									AddCommunities:      []string{standardCommunity, wellKnownCommunity},
   502  									AddLargeCommunities: []string{largeCommunity},
   503  								},
   504  							},
   505  						},
   506  					},
   507  				},
   508  			},
   509  			expectError: false,
   510  		},
   511  		{
   512  			name: "delete policy - non-matching selector",
   513  			initial: &routePolicyTestInputs{
   514  				podCIDRs: []string{
   515  					podCIDR,
   516  				},
   517  				neighbors: []v2alpha1api.CiliumBGPNeighbor{
   518  					{
   519  						PeerAddress: peerAddress,
   520  						AdvertisedPathAttributes: []v2alpha1api.CiliumBGPPathAttributes{
   521  							attrSelectAnyNode,
   522  						},
   523  					},
   524  				},
   525  				expectedPolicies: []*types.RoutePolicy{
   526  					{
   527  						Name: pathAttributesPolicyName(attrSelectAnyNode, peerAddress),
   528  						Type: types.RoutePolicyTypeExport,
   529  						Statements: []*types.RoutePolicyStatement{
   530  							{
   531  								Conditions: types.RoutePolicyConditions{
   532  									MatchNeighbors: []string{peerAddress},
   533  									MatchPrefixes: []*types.RoutePolicyPrefixMatch{
   534  										{
   535  											CIDR:         podCIDRPrefix,
   536  											PrefixLenMin: podCIDRPrefix.Bits(),
   537  											PrefixLenMax: podCIDRPrefix.Bits(),
   538  										},
   539  									},
   540  								},
   541  								Actions: types.RoutePolicyActions{
   542  									RouteAction:        types.RoutePolicyActionNone,
   543  									SetLocalPreference: attrSelectAnyNode.LocalPreference,
   544  								},
   545  							},
   546  						},
   547  					},
   548  				},
   549  			},
   550  			updated: &routePolicyTestInputs{
   551  				podCIDRs: []string{
   552  					podCIDR,
   553  				},
   554  				neighbors: []v2alpha1api.CiliumBGPNeighbor{
   555  					{
   556  						PeerAddress: peerAddress,
   557  						AdvertisedPathAttributes: []v2alpha1api.CiliumBGPPathAttributes{
   558  							attrSelectNonExistingNode, // UPDATED - not matching the node
   559  						},
   560  					},
   561  				},
   562  				expectedPolicies: nil,
   563  			},
   564  			expectError: false,
   565  		},
   566  		{
   567  			name: "error - invalid selector",
   568  			initial: &routePolicyTestInputs{
   569  				podCIDRs: []string{
   570  					podCIDR,
   571  				},
   572  				neighbors: []v2alpha1api.CiliumBGPNeighbor{
   573  					{
   574  						PeerAddress: peerAddress,
   575  						AdvertisedPathAttributes: []v2alpha1api.CiliumBGPPathAttributes{
   576  							attrSelectInvalid,
   577  						},
   578  					},
   579  				},
   580  				expectedPolicies: nil,
   581  			},
   582  			expectError: true,
   583  		},
   584  	}
   585  
   586  	for _, tt := range table {
   587  		t.Run(tt.name, func(t *testing.T) {
   588  			srvParams := types.ServerParameters{
   589  				Global: types.BGPGlobal{
   590  					ASN:        64125,
   591  					RouterID:   "127.0.0.1",
   592  					ListenPort: -1,
   593  				},
   594  			}
   595  			testSC, err := instance.NewServerWithConfig(context.Background(), log, srvParams)
   596  			require.NoError(t, err)
   597  
   598  			testSC.Config = &v2alpha1api.CiliumBGPVirtualRouter{
   599  				LocalASN:      64125,
   600  				ExportPodCIDR: ptr.To[bool](true),
   601  				Neighbors:     tt.initial.neighbors,
   602  			}
   603  
   604  			lbStore := store.NewMockBGPCPResourceStore[*v2alpha1api.CiliumLoadBalancerIPPool]()
   605  			for _, obj := range tt.initial.LBPools {
   606  				lbStore.Upsert(obj)
   607  			}
   608  
   609  			podStore := store.NewMockBGPCPResourceStore[*v2alpha1api.CiliumPodIPPool]()
   610  			for _, obj := range tt.initial.PodPools {
   611  				podStore.Upsert(obj)
   612  			}
   613  
   614  			policyReconciler := NewRoutePolicyReconciler(lbStore, podStore).Reconciler.(*RoutePolicyReconciler)
   615  			params := ReconcileParams{
   616  				CurrentServer: testSC,
   617  				DesiredConfig: testSC.Config,
   618  				CiliumNode: &v2.CiliumNode{
   619  					Spec: v2.NodeSpec{
   620  						IPAM: ipamTypes.IPAMSpec{
   621  							PodCIDRs: tt.initial.podCIDRs,
   622  							Pools: ipamTypes.IPAMPoolSpec{
   623  								Allocated: tt.initial.NodePools,
   624  							},
   625  						},
   626  					},
   627  				},
   628  			}
   629  
   630  			// Run the reconciler twice to ensure idempotency. This
   631  			// simulates the retrying behavior of the controller.
   632  			for i := 0; i < 2; i++ {
   633  				t.Run(tt.name+"-init", func(t *testing.T) {
   634  					err = policyReconciler.Reconcile(context.Background(), params)
   635  					if tt.expectError {
   636  						require.Error(t, err)
   637  						return
   638  					}
   639  					require.NoError(t, err)
   640  				})
   641  			}
   642  
   643  			// validate cached vs. expected policies
   644  			validatePoliciesMatch(t, policyReconciler.getMetadata(testSC), tt.initial.expectedPolicies)
   645  
   646  			if tt.updated == nil {
   647  				return // not testing update / remove
   648  			}
   649  
   650  			// follow-up reconcile - update:
   651  			params.DesiredConfig.Neighbors = tt.updated.neighbors
   652  			params.CiliumNode.Spec.IPAM.Pools.Allocated = tt.updated.NodePools
   653  			params.CiliumNode.Spec.IPAM.PodCIDRs = tt.updated.podCIDRs
   654  			for _, obj := range tt.updated.LBPools {
   655  				lbStore.Upsert(obj)
   656  			}
   657  			for _, obj := range tt.updated.PodPools {
   658  				podStore.Upsert(obj)
   659  			}
   660  			// Run the reconciler twice to ensure idempotency. This
   661  			// simulates the retrying behavior of the controller.
   662  			for i := 0; i < 2; i++ {
   663  				t.Run(tt.name+"-follow-up", func(t *testing.T) {
   664  					err = policyReconciler.Reconcile(context.Background(), params)
   665  					require.NoError(t, err)
   666  				})
   667  			}
   668  
   669  			// validate cached vs. expected policies
   670  			validatePoliciesMatch(t, policyReconciler.getMetadata(testSC), tt.updated.expectedPolicies)
   671  		})
   672  	}
   673  }
   674  
   675  func validatePoliciesMatch(t *testing.T, actual map[string]*types.RoutePolicy, expected []*types.RoutePolicy) {
   676  	require.Len(t, actual, len(expected))
   677  
   678  	for _, expPolicy := range expected {
   679  		policy := actual[expPolicy.Name]
   680  		require.NotNil(t, policy)
   681  		require.EqualValues(t, policy, expPolicy)
   682  	}
   683  }
   684  
   685  func TestCommunityDeduplication(t *testing.T) {
   686  	var table = []struct {
   687  		name      string
   688  		standard  []v2alpha1api.BGPStandardCommunity
   689  		wellKnown []v2alpha1api.BGPWellKnownCommunity
   690  		large     []v2alpha1api.BGPLargeCommunity
   691  		expected  []string
   692  		expectErr bool
   693  	}{
   694  		{
   695  			name:     "single standard",
   696  			standard: []v2alpha1api.BGPStandardCommunity{"64125:100"},
   697  			expected: []string{"64125:100"},
   698  		},
   699  		{
   700  			name:      "single well-known",
   701  			wellKnown: []v2alpha1api.BGPWellKnownCommunity{"no-advertise"},
   702  			expected:  []string{"no-advertise"},
   703  		},
   704  		{
   705  			name:     "single large",
   706  			large:    []v2alpha1api.BGPLargeCommunity{"64125:4294967295:100"},
   707  			expected: []string{"64125:4294967295:100"},
   708  		},
   709  		{
   710  			name:     "duplicate standard",
   711  			standard: []v2alpha1api.BGPStandardCommunity{"0:100", "100", "0:101", "0:100"},
   712  			expected: []string{"0:100", "0:101"},
   713  		},
   714  		{
   715  			name:      "duplicate well-known",
   716  			wellKnown: []v2alpha1api.BGPWellKnownCommunity{"no-export", "no-advertise", "no-export"},
   717  			expected:  []string{"no-export", "no-advertise"},
   718  		},
   719  		{
   720  			name:      "invalid standard",
   721  			standard:  []v2alpha1api.BGPStandardCommunity{"64125:100", "999999:999999", "64125:101"},
   722  			expectErr: true,
   723  		},
   724  		{
   725  			name:      "invalid well-known",
   726  			wellKnown: []v2alpha1api.BGPWellKnownCommunity{"no-export", "NON-EXISTING"},
   727  			expectErr: true,
   728  		},
   729  		{
   730  			name:     "duplicate large",
   731  			large:    []v2alpha1api.BGPLargeCommunity{"64125:4294967295:100", "64125:4294967295:200", "64125:4294967295:200"},
   732  			expected: []string{"64125:4294967295:100", "64125:4294967295:200"},
   733  		},
   734  		{
   735  			name:      "standard + well-known duplicated",
   736  			standard:  []v2alpha1api.BGPStandardCommunity{"64125:100", "64125:101", "65535:65282"},
   737  			wellKnown: []v2alpha1api.BGPWellKnownCommunity{"no-export", "no-advertise"}, // no-advertise = "65535:65282"
   738  			expected:  []string{"64125:100", "64125:101", "65535:65282", "no-export"},
   739  		},
   740  	}
   741  	for _, tt := range table {
   742  		t.Run(tt.name, func(t *testing.T) {
   743  			res, err := mergeAndDedupCommunities(tt.standard, tt.wellKnown)
   744  			if tt.expectErr {
   745  				require.Error(t, err)
   746  				return
   747  			}
   748  			require.NoError(t, err)
   749  
   750  			res2 := dedupLargeCommunities(tt.large)
   751  
   752  			require.EqualValues(t, tt.expected, append(res, res2...))
   753  		})
   754  	}
   755  }