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  }