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  }