github.com/cilium/cilium@v1.16.2/pkg/bgpv1/test/neighbor_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 "sort" 10 "testing" 11 "time" 12 13 "github.com/stretchr/testify/require" 14 meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 15 "k8s.io/utils/ptr" 16 17 "github.com/cilium/cilium/api/v1/models" 18 "github.com/cilium/cilium/pkg/bgpv1/types" 19 cilium_api_v2alpha1 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1" 20 "github.com/cilium/cilium/pkg/testutils" 21 ) 22 23 var ( 24 // maxNeighborTestDuration is allowed time for the Neighbor tests execution 25 // (on average we need about 35-40s to finish the tests due to BGP timers etc.) 26 maxNeighborTestDuration = 1 * time.Minute 27 ) 28 29 // peeringState helper struct containing peering information of BGP neighbor 30 type peeringState struct { 31 peerASN uint32 32 peerAddr string 33 peerSession string 34 holdTimeSeconds int64 // applied hold time, as negotiated with the peer during the session setup 35 gracefulRestartEnabled bool 36 gracefulRestartTime int64 // configured restart time 37 } 38 39 // Test_NeighborAddDel validates neighbor add and delete are working as expected. Test validates this using 40 // peering status which is reported from BGP control plane. 41 // Topology - (BGP CP) === (2 x gobgp instances) 42 func Test_NeighborAddDel(t *testing.T) { 43 testutils.PrivilegedTest(t) 44 45 var steps = []struct { 46 description string 47 neighbors []cilium_api_v2alpha1.CiliumBGPNeighbor 48 waitState []string 49 expectedPeerStates []peeringState 50 }{ 51 { 52 description: "add two neighbors", 53 neighbors: []cilium_api_v2alpha1.CiliumBGPNeighbor{ 54 { 55 PeerAddress: dummies[instance1Link].ipv4.String(), 56 PeerASN: int64(gobgpASN), 57 HoldTimeSeconds: ptr.To[int32](9), // must be lower than default (90s) to be applied on the peer 58 KeepAliveTimeSeconds: ptr.To[int32](1), // must be lower than HoldTime 59 AuthSecretRef: ptr.To[string]("a-secret"), 60 }, 61 { 62 PeerAddress: dummies[instance2Link].ipv4.String(), 63 PeerASN: int64(gobgpASN2), 64 HoldTimeSeconds: ptr.To[int32](6), // must be lower than default (90s) to be applied on the peer 65 KeepAliveTimeSeconds: ptr.To[int32](1), // must be lower than HoldTime 66 }, 67 }, 68 waitState: []string{"ESTABLISHED"}, 69 expectedPeerStates: []peeringState{ 70 { 71 peerASN: gobgpASN, 72 peerAddr: dummies[instance1Link].ipv4.Addr().String(), 73 peerSession: types.SessionEstablished.String(), 74 holdTimeSeconds: 9, 75 }, 76 { 77 peerASN: gobgpASN2, 78 peerAddr: dummies[instance2Link].ipv4.Addr().String(), 79 peerSession: types.SessionEstablished.String(), 80 holdTimeSeconds: 6, 81 }, 82 }, 83 }, 84 { 85 description: "update both neighbors", 86 neighbors: []cilium_api_v2alpha1.CiliumBGPNeighbor{ 87 { 88 PeerAddress: dummies[instance1Link].ipv4.String(), 89 PeerASN: int64(gobgpASN), 90 HoldTimeSeconds: ptr.To[int32](6), // updated, must be lower than the previous value to be applied on the peer 91 KeepAliveTimeSeconds: ptr.To[int32](1), // must be lower than HoldTime 92 AuthSecretRef: ptr.To[string]("a-secret"), 93 }, 94 { 95 PeerAddress: dummies[instance2Link].ipv4.String(), 96 PeerASN: int64(gobgpASN2), 97 HoldTimeSeconds: ptr.To[int32](3), // updated, must be lower than the previous value to be applied on the peer 98 KeepAliveTimeSeconds: ptr.To[int32](1), // must be lower than HoldTime 99 }, 100 }, 101 waitState: []string{"ESTABLISHED"}, 102 expectedPeerStates: []peeringState{ 103 { 104 peerASN: gobgpASN, 105 peerAddr: dummies[instance1Link].ipv4.Addr().String(), 106 peerSession: types.SessionEstablished.String(), 107 holdTimeSeconds: 6, 108 }, 109 { 110 peerASN: gobgpASN2, 111 peerAddr: dummies[instance2Link].ipv4.Addr().String(), 112 peerSession: types.SessionEstablished.String(), 113 holdTimeSeconds: 3, 114 }, 115 }, 116 }, 117 { 118 description: "delete both neighbors", 119 neighbors: []cilium_api_v2alpha1.CiliumBGPNeighbor{}, 120 waitState: []string{"IDLE", "ACTIVE"}, 121 expectedPeerStates: nil, 122 }, 123 } 124 125 testCtx, testDone := context.WithTimeout(context.Background(), maxNeighborTestDuration) 126 defer testDone() 127 128 // test setup, we configure two gobgp instances here. 129 gobgpInstances, fixture, cleanup, err := setup(testCtx, t, []gobgpConfig{gobgpConfPassword, gobgpConf2}, newFixtureConf()) 130 require.NoError(t, err) 131 require.Len(t, gobgpInstances, 2) 132 defer cleanup() 133 134 for _, step := range steps { 135 t.Run(step.description, func(t *testing.T) { 136 // update bgp policy with neighbors defined in test step 137 policyObj := newPolicyObj(policyConfig{ 138 nodeSelector: labels, 139 virtualRouters: []cilium_api_v2alpha1.CiliumBGPVirtualRouter{ 140 { 141 LocalASN: int64(ciliumASN), 142 ExportPodCIDR: ptr.To[bool](true), 143 Neighbors: step.neighbors, 144 }, 145 }, 146 }) 147 _, err = fixture.policyClient.Update(testCtx, &policyObj, meta_v1.UpdateOptions{}) 148 require.NoError(t, err, step.description) 149 150 // wait for peers to reach expected state 151 for _, gobgpInstance := range gobgpInstances { 152 err = gobgpInstance.waitForSessionState(testCtx, step.waitState) 153 require.NoError(t, err, step.description) 154 } 155 156 deadline, _ := testCtx.Deadline() 157 outstanding := time.Until(deadline) 158 require.Greater(t, outstanding, 0*time.Second, "test context deadline exceeded") 159 160 peerStatesMatch := func() bool { 161 // validate expected state vs state reported by BGP CP 162 var peers []*models.BgpPeer 163 peers, err = fixture.bgp.BGPMgr.GetPeers(testCtx) 164 require.NoError(t, err, step.description) 165 166 var runningState []peeringState 167 for _, peer := range peers { 168 runningState = append(runningState, peeringState{ 169 peerASN: uint32(peer.PeerAsn), 170 peerAddr: peer.PeerAddress, 171 peerSession: peer.SessionState, 172 holdTimeSeconds: peer.AppliedHoldTimeSeconds, 173 }) 174 } 175 return peeringStatesEqual(t, step.expectedPeerStates, runningState) 176 } 177 178 // Retry peerStatesMatch once per second until the test context deadline. 179 // We may need to retry as remote peer's session state does not have to immediately match our 180 // session state (e.g. peer may be already in Established but we still in OpenConfirm 181 // until we receive a Keepalive from the peer). 182 require.Eventually(t, peerStatesMatch, outstanding, 1*time.Second, step.description) 183 }) 184 } 185 } 186 187 // Test_NeighborGracefulRestart tests graceful restart configuration knobs with single peer. 188 func Test_NeighborGracefulRestart(t *testing.T) { 189 testutils.PrivilegedTest(t) 190 191 var steps = []struct { 192 description string 193 neighbor cilium_api_v2alpha1.CiliumBGPNeighbor 194 waitState []string 195 expectedPeerState peeringState 196 }{ 197 { 198 description: "add neighbor with defaults", 199 neighbor: cilium_api_v2alpha1.CiliumBGPNeighbor{ 200 PeerAddress: dummies[instance1Link].ipv4.String(), 201 PeerASN: int64(gobgpASN), 202 }, 203 waitState: []string{"ESTABLISHED"}, 204 expectedPeerState: peeringState{ 205 peerASN: gobgpASN, 206 peerAddr: dummies[instance1Link].ipv4.Addr().String(), 207 peerSession: types.SessionEstablished.String(), 208 }, 209 }, 210 { 211 description: "update graceful restart with defaults", 212 neighbor: cilium_api_v2alpha1.CiliumBGPNeighbor{ 213 PeerAddress: dummies[instance1Link].ipv4.String(), 214 PeerASN: int64(gobgpASN), 215 GracefulRestart: &cilium_api_v2alpha1.CiliumBGPNeighborGracefulRestart{ 216 Enabled: true, 217 }, 218 }, 219 waitState: []string{"ESTABLISHED"}, 220 expectedPeerState: peeringState{ 221 peerASN: gobgpASN, 222 peerAddr: dummies[instance1Link].ipv4.Addr().String(), 223 peerSession: types.SessionEstablished.String(), 224 gracefulRestartEnabled: true, 225 gracefulRestartTime: int64(cilium_api_v2alpha1.DefaultBGPGRRestartTimeSeconds), 226 }, 227 }, 228 { 229 description: "update graceful restart, restart time", 230 neighbor: cilium_api_v2alpha1.CiliumBGPNeighbor{ 231 PeerAddress: dummies[instance1Link].ipv4.String(), 232 PeerASN: int64(gobgpASN), 233 GracefulRestart: &cilium_api_v2alpha1.CiliumBGPNeighborGracefulRestart{ 234 Enabled: true, 235 RestartTimeSeconds: ptr.To[int32](20), 236 }, 237 }, 238 waitState: []string{"ESTABLISHED"}, 239 expectedPeerState: peeringState{ 240 peerASN: gobgpASN, 241 peerAddr: dummies[instance1Link].ipv4.Addr().String(), 242 peerSession: types.SessionEstablished.String(), 243 gracefulRestartEnabled: true, 244 gracefulRestartTime: 20, 245 }, 246 }, 247 { 248 description: "disable graceful restart", 249 neighbor: cilium_api_v2alpha1.CiliumBGPNeighbor{ 250 PeerAddress: dummies[instance1Link].ipv4.String(), 251 PeerASN: int64(gobgpASN), 252 GracefulRestart: &cilium_api_v2alpha1.CiliumBGPNeighborGracefulRestart{ 253 Enabled: false, 254 }, 255 }, 256 waitState: []string{"ESTABLISHED"}, 257 expectedPeerState: peeringState{ 258 peerASN: gobgpASN, 259 peerAddr: dummies[instance1Link].ipv4.Addr().String(), 260 peerSession: types.SessionEstablished.String(), 261 gracefulRestartEnabled: false, 262 }, 263 }, 264 } 265 266 // This test run can take upto a minute 267 testCtx, testDone := context.WithTimeout(context.Background(), maxGracefulRestartTestDuration) 268 defer testDone() 269 270 // test setup, we configure single gobgp instance here. 271 gobgpInstances, fixture, cleanup, err := setup(testCtx, t, []gobgpConfig{gobgpConf}, newFixtureConf()) 272 require.NoError(t, err) 273 require.Len(t, gobgpInstances, 1) 274 defer cleanup() 275 276 for _, step := range steps { 277 t.Run(step.description, func(t *testing.T) { 278 // update bgp policy with neighbors defined in test step 279 policyObj := newPolicyObj(policyConfig{ 280 nodeSelector: labels, 281 virtualRouters: []cilium_api_v2alpha1.CiliumBGPVirtualRouter{ 282 { 283 LocalASN: int64(ciliumASN), 284 ExportPodCIDR: ptr.To[bool](true), 285 Neighbors: []cilium_api_v2alpha1.CiliumBGPNeighbor{step.neighbor}, 286 }, 287 }, 288 }) 289 _, err = fixture.policyClient.Update(testCtx, &policyObj, meta_v1.UpdateOptions{}) 290 require.NoError(t, err) 291 292 // wait for peers to reach expected state 293 err = gobgpInstances[0].waitForSessionState(testCtx, step.waitState) 294 require.NoError(t, err) 295 296 deadline, _ := testCtx.Deadline() 297 outstanding := time.Until(deadline) 298 require.Greater(t, outstanding, 0*time.Second, "test context deadline exceeded") 299 300 peerStatesMatch := func() bool { 301 // validate expected state vs state reported by BGP CP 302 var peers []*models.BgpPeer 303 peers, err = fixture.bgp.BGPMgr.GetPeers(testCtx) 304 require.NoError(t, err, step.description) 305 require.Len(t, peers, 1) 306 307 runningPeerState := peeringState{ 308 peerASN: uint32(peers[0].PeerAsn), 309 peerAddr: peers[0].PeerAddress, 310 peerSession: peers[0].SessionState, 311 gracefulRestartEnabled: peers[0].GracefulRestart.Enabled, 312 gracefulRestartTime: peers[0].GracefulRestart.RestartTimeSeconds, 313 } 314 return peeringStatesEqual(t, []peeringState{step.expectedPeerState}, []peeringState{runningPeerState}) 315 } 316 317 // Retry peerStatesMatch once per second until the test context deadline. 318 // We may need to retry as remote peer's session state does not have to immediately match our 319 // session state (e.g. peer may be already in Established but we still in OpenConfirm 320 // until we receive a Keepalive from the peer). 321 require.Eventually(t, peerStatesMatch, outstanding, 1*time.Second) 322 }) 323 } 324 } 325 326 func peeringStatesEqual(t *testing.T, expected, actual []peeringState) bool { 327 sort.Slice(expected, func(i, j int) bool { 328 return expected[i].peerASN < expected[j].peerASN 329 }) 330 sort.Slice(actual, func(i, j int) bool { 331 return actual[i].peerASN < actual[j].peerASN 332 }) 333 equal := reflect.DeepEqual(expected, actual) 334 if !equal { 335 t.Logf("peering states not (yet) equal - expected: %v, actual: %v", expected, actual) 336 } 337 return equal 338 }