github.com/cilium/cilium@v1.16.2/pkg/bgpv1/manager/reconciler/neighbor_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 "errors" 9 "net/netip" 10 "testing" 11 12 "github.com/stretchr/testify/require" 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 v2alpha1api "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1" 19 slim_corev1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/api/core/v1" 20 slim_metav1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1" 21 "github.com/cilium/cilium/pkg/option" 22 ) 23 24 func FakeSecretStore(secrets map[string][]byte) store.BGPCPResourceStore[*slim_corev1.Secret] { 25 store := store.NewMockBGPCPResourceStore[*slim_corev1.Secret]() 26 for k, v := range secrets { 27 store.Upsert(&slim_corev1.Secret{ 28 ObjectMeta: slim_metav1.ObjectMeta{ 29 Namespace: "bgp-secrets", 30 Name: k, 31 }, 32 Data: map[string]slim_corev1.Bytes{"password": slim_corev1.Bytes(v)}, 33 }) 34 } 35 return store 36 } 37 38 // TestNeighborReconciler confirms the `neighborReconciler` function configures 39 // the desired BGP neighbors given a CiliumBGPVirtualRouter configuration. 40 func TestNeighborReconciler(t *testing.T) { 41 type checkTimers struct { 42 holdTimer bool 43 connectRetryTimer bool 44 keepaliveTimer bool 45 grRestartTime bool 46 } 47 48 table := []struct { 49 // name of the test 50 name string 51 // existing neighbors, expanded to CiliumBGPNeighbor during test 52 neighbors []v2alpha1api.CiliumBGPNeighbor 53 // new neighbors to configure, expanded into CiliumBGPNeighbor. 54 // 55 // this is the resulting neighbors we expect on the BgpServer. 56 newNeighbors []v2alpha1api.CiliumBGPNeighbor 57 // secretStore passed to the test, provides a way to fetch secrets (use FakeSecretStore above). 58 secretStore store.BGPCPResourceStore[*slim_corev1.Secret] 59 // checks validates set timer values 60 checks checkTimers 61 // expected secret if set. 62 expectedSecret string 63 // expected password if set. 64 expectedPassword string 65 // error provided or nil 66 err error 67 }{ 68 { 69 name: "no change", 70 neighbors: []v2alpha1api.CiliumBGPNeighbor{ 71 {PeerASN: 64124, PeerAddress: "192.168.0.1/32"}, 72 {PeerASN: 64124, PeerAddress: "192.168.0.2/32"}, 73 }, 74 newNeighbors: []v2alpha1api.CiliumBGPNeighbor{ 75 {PeerASN: 64124, PeerAddress: "192.168.0.1/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort)}, 76 {PeerASN: 64124, PeerAddress: "192.168.0.2/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort)}, 77 }, 78 err: nil, 79 }, 80 { 81 name: "change peer ASN", 82 neighbors: []v2alpha1api.CiliumBGPNeighbor{ 83 {PeerASN: 64124, PeerAddress: "192.168.0.1/32"}, 84 {PeerASN: 64124, PeerAddress: "192.168.0.2/32"}, 85 }, 86 newNeighbors: []v2alpha1api.CiliumBGPNeighbor{ 87 {PeerASN: 64125, PeerAddress: "192.168.0.1/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort)}, 88 {PeerASN: 64125, PeerAddress: "192.168.0.2/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort)}, 89 }, 90 err: nil, 91 }, 92 { 93 name: "neighbor with peer port", 94 neighbors: []v2alpha1api.CiliumBGPNeighbor{ 95 {PeerASN: 64124, PeerAddress: "192.168.0.1/32", PeerPort: ptr.To[int32](42424)}, 96 {PeerASN: 64124, PeerAddress: "192.168.0.2/32"}, 97 }, 98 newNeighbors: []v2alpha1api.CiliumBGPNeighbor{ 99 {PeerASN: 64124, PeerAddress: "192.168.0.1/32", PeerPort: ptr.To[int32](42424)}, 100 {PeerASN: 64124, PeerAddress: "192.168.0.2/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort)}, 101 }, 102 err: nil, 103 }, 104 { 105 name: "additional neighbor", 106 neighbors: []v2alpha1api.CiliumBGPNeighbor{ 107 {PeerASN: 64124, PeerAddress: "192.168.0.1/32"}, 108 {PeerASN: 64124, PeerAddress: "192.168.0.2/32"}, 109 }, 110 newNeighbors: []v2alpha1api.CiliumBGPNeighbor{ 111 {PeerASN: 64124, PeerAddress: "192.168.0.1/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort)}, 112 {PeerASN: 64124, PeerAddress: "192.168.0.2/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort)}, 113 {PeerASN: 64124, PeerAddress: "192.168.0.3/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort)}, 114 }, 115 err: nil, 116 }, 117 { 118 name: "remove neighbor", 119 neighbors: []v2alpha1api.CiliumBGPNeighbor{ 120 {PeerASN: 64124, PeerAddress: "192.168.0.1/32"}, 121 {PeerASN: 64124, PeerAddress: "192.168.0.2/32"}, 122 {PeerASN: 64124, PeerAddress: "192.168.0.3/32"}, 123 }, 124 newNeighbors: []v2alpha1api.CiliumBGPNeighbor{ 125 {PeerASN: 64124, PeerAddress: "192.168.0.1/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort)}, 126 {PeerASN: 64124, PeerAddress: "192.168.0.2/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort)}, 127 }, 128 err: nil, 129 }, 130 { 131 name: "update neighbor", 132 neighbors: []v2alpha1api.CiliumBGPNeighbor{ 133 {PeerASN: 64124, PeerAddress: "192.168.0.1/32", ConnectRetryTimeSeconds: ptr.To[int32](120)}, 134 {PeerASN: 64124, PeerAddress: "192.168.0.2/32", ConnectRetryTimeSeconds: ptr.To[int32](120)}, 135 {PeerASN: 64124, PeerAddress: "192.168.0.3/32", ConnectRetryTimeSeconds: ptr.To[int32](120)}, 136 }, 137 newNeighbors: []v2alpha1api.CiliumBGPNeighbor{ 138 {PeerASN: 64124, PeerAddress: "192.168.0.1/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort), ConnectRetryTimeSeconds: ptr.To[int32](99)}, 139 {PeerASN: 64124, PeerAddress: "192.168.0.2/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort), ConnectRetryTimeSeconds: ptr.To[int32](120)}, 140 {PeerASN: 64124, PeerAddress: "192.168.0.3/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort), ConnectRetryTimeSeconds: ptr.To[int32](120)}, 141 }, 142 checks: checkTimers{ 143 connectRetryTimer: true, 144 }, 145 err: nil, 146 }, 147 { 148 name: "update neighbor - graceful restart", 149 neighbors: []v2alpha1api.CiliumBGPNeighbor{ 150 {PeerASN: 64124, PeerAddress: "192.168.0.1/32", GracefulRestart: &v2alpha1api.CiliumBGPNeighborGracefulRestart{ 151 Enabled: true, 152 RestartTimeSeconds: ptr.To[int32](v2alpha1api.DefaultBGPGRRestartTimeSeconds), 153 }}, 154 {PeerASN: 64124, PeerAddress: "192.168.0.2/32", GracefulRestart: &v2alpha1api.CiliumBGPNeighborGracefulRestart{ 155 Enabled: true, 156 RestartTimeSeconds: ptr.To[int32](v2alpha1api.DefaultBGPGRRestartTimeSeconds), 157 }}, 158 {PeerASN: 64124, PeerAddress: "192.168.0.3/32", GracefulRestart: &v2alpha1api.CiliumBGPNeighborGracefulRestart{ 159 Enabled: true, 160 RestartTimeSeconds: ptr.To[int32](v2alpha1api.DefaultBGPGRRestartTimeSeconds), 161 }}, 162 }, 163 newNeighbors: []v2alpha1api.CiliumBGPNeighbor{ 164 {PeerASN: 64124, PeerAddress: "192.168.0.1/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort), GracefulRestart: &v2alpha1api.CiliumBGPNeighborGracefulRestart{ 165 Enabled: false, 166 RestartTimeSeconds: ptr.To[int32](0), 167 }}, 168 {PeerASN: 64124, PeerAddress: "192.168.0.2/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort), GracefulRestart: &v2alpha1api.CiliumBGPNeighborGracefulRestart{ 169 Enabled: true, 170 RestartTimeSeconds: ptr.To[int32](v2alpha1api.DefaultBGPGRRestartTimeSeconds), 171 }}, 172 {PeerASN: 64124, PeerAddress: "192.168.0.3/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort), GracefulRestart: &v2alpha1api.CiliumBGPNeighborGracefulRestart{ 173 Enabled: true, 174 RestartTimeSeconds: ptr.To[int32](v2alpha1api.DefaultBGPGRRestartTimeSeconds), 175 }}, 176 }, 177 checks: checkTimers{ 178 grRestartTime: true, 179 }, 180 err: nil, 181 }, 182 { 183 name: "update neighbor port", 184 neighbors: []v2alpha1api.CiliumBGPNeighbor{ 185 {PeerASN: 64124, PeerAddress: "192.168.0.1/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort)}, 186 {PeerASN: 64124, PeerAddress: "192.168.0.2/32"}, 187 }, 188 newNeighbors: []v2alpha1api.CiliumBGPNeighbor{ 189 {PeerASN: 64124, PeerAddress: "192.168.0.1/32", PeerPort: ptr.To[int32](42424)}, 190 {PeerASN: 64124, PeerAddress: "192.168.0.2/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort)}, 191 }, 192 err: nil, 193 }, 194 { 195 name: "remove all neighbors", 196 neighbors: []v2alpha1api.CiliumBGPNeighbor{ 197 {PeerASN: 64124, PeerAddress: "192.168.0.1/32"}, 198 {PeerASN: 64124, PeerAddress: "192.168.0.2/32"}, 199 {PeerASN: 64124, PeerAddress: "192.168.0.3/32"}, 200 }, 201 newNeighbors: []v2alpha1api.CiliumBGPNeighbor{}, 202 err: nil, 203 }, 204 { 205 name: "add neighbor with a password", 206 neighbors: []v2alpha1api.CiliumBGPNeighbor{}, 207 newNeighbors: []v2alpha1api.CiliumBGPNeighbor{ 208 {PeerASN: 64124, PeerAddress: "192.168.0.1/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort), AuthSecretRef: ptr.To[string]("a-secret")}, 209 }, 210 secretStore: FakeSecretStore(map[string][]byte{"a-secret": []byte("a-password")}), 211 expectedSecret: "a-secret", 212 expectedPassword: "a-password", 213 err: nil, 214 }, 215 { 216 name: "neighbor's password secret not found", 217 neighbors: []v2alpha1api.CiliumBGPNeighbor{ 218 {PeerASN: 64124, PeerAddress: "192.168.0.1/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort)}, 219 }, 220 newNeighbors: []v2alpha1api.CiliumBGPNeighbor{ 221 {PeerASN: 64124, PeerAddress: "192.168.0.1/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort), AuthSecretRef: ptr.To[string]("bad-secret")}, 222 }, 223 secretStore: FakeSecretStore(map[string][]byte{}), 224 err: nil, 225 }, 226 { 227 name: "bad secret store, returns error", 228 neighbors: []v2alpha1api.CiliumBGPNeighbor{ 229 {PeerASN: 64124, PeerAddress: "192.168.0.1/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort)}, 230 }, 231 newNeighbors: []v2alpha1api.CiliumBGPNeighbor{ 232 {PeerASN: 64124, PeerAddress: "192.168.0.1/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort), AuthSecretRef: ptr.To[string]("a-secret")}, 233 }, 234 err: errors.New("fetch secret error"), 235 }, 236 { 237 name: "neighbor's secret updated", 238 neighbors: []v2alpha1api.CiliumBGPNeighbor{ 239 {PeerASN: 64124, PeerAddress: "192.168.0.1/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort), AuthSecretRef: ptr.To[string]("a-secret")}, 240 }, 241 newNeighbors: []v2alpha1api.CiliumBGPNeighbor{ 242 {PeerASN: 64124, PeerAddress: "192.168.0.1/32", PeerPort: ptr.To[int32](v2alpha1api.DefaultBGPPeerPort), AuthSecretRef: ptr.To[string]("another-secret")}, 243 }, 244 secretStore: FakeSecretStore(map[string][]byte{"another-secret": []byte("another-password")}), 245 expectedSecret: "another-secret", 246 expectedPassword: "another-password", 247 }, 248 } 249 for _, tt := range table { 250 t.Run(tt.name, func(t *testing.T) { 251 // our test BgpServer with our original router ID and local port 252 srvParams := types.ServerParameters{ 253 Global: types.BGPGlobal{ 254 ASN: 64125, 255 RouterID: "127.0.0.1", 256 ListenPort: -1, 257 }, 258 } 259 testSC, err := instance.NewServerWithConfig(context.Background(), log, srvParams) 260 if err != nil { 261 t.Fatalf("failed to create test BgpServer: %v", err) 262 } 263 t.Cleanup(func() { 264 testSC.Server.Stop() 265 }) 266 267 r := NewNeighborReconciler(tt.secretStore, &option.DaemonConfig{BGPSecretsNamespace: "bgp-secrets"}).Reconciler 268 269 neighborReconciler := r.(*NeighborReconciler) 270 271 for _, n := range tt.neighbors { 272 n.SetDefaults() 273 274 tcpPassword, err := neighborReconciler.fetchPeerPassword(testSC, &n) 275 require.NoError(t, err) 276 277 neighborReconciler.updateMetadata(testSC, n.DeepCopy(), tcpPassword) 278 279 testSC.Server.AddNeighbor(context.Background(), types.NeighborRequest{ 280 Neighbor: &n, 281 Password: tcpPassword, 282 }) 283 } 284 285 // create new virtual router config with desired neighbors 286 newc := &v2alpha1api.CiliumBGPVirtualRouter{ 287 LocalASN: 64125, 288 Neighbors: []v2alpha1api.CiliumBGPNeighbor{}, 289 } 290 newc.Neighbors = append(newc.Neighbors, tt.newNeighbors...) 291 newc.SetDefaults() 292 293 params := ReconcileParams{ 294 CurrentServer: testSC, 295 DesiredConfig: newc, 296 } 297 298 // Run the reconciler twice to ensure idempotency. This 299 // simulates the retrying behavior of the controller. 300 for i := 0; i < 2; i++ { 301 t.Run(tt.name, func(t *testing.T) { 302 err = neighborReconciler.Reconcile(context.Background(), params) 303 if (tt.err == nil) != (err == nil) { 304 t.Fatalf("want error: %v, got: %v", (tt.err == nil), err) 305 } 306 }) 307 } 308 309 // clear out secret ref if one isn't expected 310 if tt.expectedSecret == "" { 311 for i := range tt.newNeighbors { 312 tt.newNeighbors[i].AuthSecretRef = nil 313 } 314 } 315 316 // check testSC for desired neighbors 317 var getPeerResp types.GetPeerStateResponse 318 getPeerResp, err = testSC.Server.GetPeerState(context.Background()) 319 if err != nil { 320 t.Fatalf("failed creating test BgpServer: %v", err) 321 } 322 var runningPeers []v2alpha1api.CiliumBGPNeighbor 323 324 for _, peer := range getPeerResp.Peers { 325 toCiliumPeer := v2alpha1api.CiliumBGPNeighbor{ 326 PeerAddress: toHostPrefix(peer.PeerAddress), 327 PeerPort: ptr.To[int32](int32(peer.PeerPort)), 328 PeerASN: peer.PeerAsn, 329 } 330 331 if tt.checks.holdTimer { 332 toCiliumPeer.HoldTimeSeconds = ptr.To[int32](int32(peer.ConfiguredHoldTimeSeconds)) 333 } 334 335 if tt.checks.connectRetryTimer { 336 toCiliumPeer.ConnectRetryTimeSeconds = ptr.To[int32](int32(peer.ConnectRetryTimeSeconds)) 337 } 338 339 if tt.checks.keepaliveTimer { 340 toCiliumPeer.KeepAliveTimeSeconds = ptr.To[int32](int32(peer.ConfiguredKeepAliveTimeSeconds)) 341 } 342 343 if tt.checks.grRestartTime { 344 toCiliumPeer.GracefulRestart = &v2alpha1api.CiliumBGPNeighborGracefulRestart{ 345 Enabled: peer.GracefulRestart.Enabled, 346 RestartTimeSeconds: ptr.To[int32](int32(peer.GracefulRestart.RestartTimeSeconds)), 347 } 348 } 349 350 // Check the API correctly reports a password was used. 351 require.Equal(t, tt.expectedPassword != "", peer.TCPPasswordEnabled) 352 if tt.expectedPassword != "" && peer.TCPPasswordEnabled { 353 toCiliumPeer.AuthSecretRef = ptr.To[string](tt.expectedSecret) 354 } 355 356 runningPeers = append(runningPeers, toCiliumPeer) 357 } 358 359 require.ElementsMatch(t, tt.newNeighbors, runningPeers) 360 }) 361 } 362 } 363 364 // hostPrefixLen returns addr/32 for ipv4 address and addr/128 for ipv6 address 365 func toHostPrefix(addr string) string { 366 addrNet := netip.MustParseAddr(addr) 367 bits := 32 368 if addrNet.Is6() { 369 bits = 128 370 } 371 return netip.PrefixFrom(addrNet, bits).String() 372 }