github.com/cilium/cilium@v1.16.2/pkg/bgpv1/manager/reconcilerv2/neighbor.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package reconcilerv2
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"net/netip"
    10  
    11  	"github.com/cilium/hive/cell"
    12  	"github.com/sirupsen/logrus"
    13  
    14  	"github.com/cilium/cilium/pkg/bgpv1/manager/instance"
    15  	"github.com/cilium/cilium/pkg/bgpv1/manager/store"
    16  	"github.com/cilium/cilium/pkg/bgpv1/types"
    17  	"github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1"
    18  	"github.com/cilium/cilium/pkg/k8s/resource"
    19  	slim_corev1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/api/core/v1"
    20  	"github.com/cilium/cilium/pkg/option"
    21  )
    22  
    23  // NeighborReconciler is a ConfigReconciler which reconciles the peers of the
    24  // provided BGP server with the provided CiliumBGPVirtualRouter.
    25  type NeighborReconciler struct {
    26  	Logger       logrus.FieldLogger
    27  	SecretStore  store.BGPCPResourceStore[*slim_corev1.Secret]
    28  	PeerConfig   store.BGPCPResourceStore[*v2alpha1.CiliumBGPPeerConfig]
    29  	DaemonConfig *option.DaemonConfig
    30  }
    31  
    32  type NeighborReconcilerOut struct {
    33  	cell.Out
    34  
    35  	Reconciler ConfigReconciler `group:"bgp-config-reconciler-v2"`
    36  }
    37  
    38  type NeighborReconcilerIn struct {
    39  	cell.In
    40  	Logger       logrus.FieldLogger
    41  	SecretStore  store.BGPCPResourceStore[*slim_corev1.Secret]
    42  	PeerConfig   store.BGPCPResourceStore[*v2alpha1.CiliumBGPPeerConfig]
    43  	DaemonConfig *option.DaemonConfig
    44  }
    45  
    46  func NewNeighborReconciler(params NeighborReconcilerIn) NeighborReconcilerOut {
    47  	logger := params.Logger.WithField(types.ReconcilerLogField, "Neighbor")
    48  
    49  	return NeighborReconcilerOut{
    50  		Reconciler: &NeighborReconciler{
    51  			Logger:       logger,
    52  			SecretStore:  params.SecretStore,
    53  			PeerConfig:   params.PeerConfig,
    54  			DaemonConfig: params.DaemonConfig,
    55  		},
    56  	}
    57  }
    58  
    59  // PeerData keeps a peer and its configuration. It also keeps the TCP password from secret store.
    60  // +deepequal-gen=true
    61  // Note:  If you change PeerDate, do not forget to 'make generate-k8s-api', which will update DeepEqual method.
    62  type PeerData struct {
    63  	Peer     *v2alpha1.CiliumBGPNodePeer
    64  	Config   *v2alpha1.CiliumBGPPeerConfigSpec
    65  	Password string
    66  }
    67  
    68  // NeighborReconcilerMetadata keeps a map of running peers to peer configuration.
    69  // key is the peer name.
    70  type NeighborReconcilerMetadata map[string][]*PeerData
    71  
    72  func (r *NeighborReconciler) getMetadata(i *instance.BGPInstance) NeighborReconcilerMetadata {
    73  	if _, found := i.Metadata[r.Name()]; !found {
    74  		i.Metadata[r.Name()] = make(NeighborReconcilerMetadata)
    75  	}
    76  	return i.Metadata[r.Name()].(NeighborReconcilerMetadata)
    77  }
    78  
    79  func (r *NeighborReconciler) upsertMetadata(i *instance.BGPInstance, instanceName string, d *PeerData) {
    80  	if i == nil || d == nil {
    81  		return
    82  	}
    83  
    84  	neighMetadata := r.getMetadata(i)
    85  
    86  	peers, exists := neighMetadata[instanceName]
    87  	if !exists {
    88  		neighMetadata[instanceName] = []*PeerData{d}
    89  		return
    90  	}
    91  
    92  	found := false
    93  	for i, p := range peers {
    94  		if p.Peer.Name == d.Peer.Name {
    95  			peers[i] = d
    96  			found = true
    97  			break
    98  		}
    99  	}
   100  
   101  	if !found {
   102  		peers = append(peers, d)
   103  	}
   104  
   105  	neighMetadata[instanceName] = peers
   106  }
   107  
   108  func (r *NeighborReconciler) deleteMetadata(i *instance.BGPInstance, instanceName string, d *PeerData) {
   109  	if i == nil || d == nil {
   110  		return
   111  	}
   112  
   113  	neighMetadata := r.getMetadata(i)
   114  	peers, exists := neighMetadata[instanceName]
   115  	if !exists {
   116  		return
   117  	}
   118  
   119  	for i, p := range peers {
   120  		if p.Peer.Name == d.Peer.Name {
   121  			peers[i] = peers[len(peers)-1]
   122  			peers = peers[:len(peers)-1]
   123  			break
   124  		}
   125  	}
   126  
   127  	neighMetadata[instanceName] = peers
   128  }
   129  
   130  func (r *NeighborReconciler) Name() string {
   131  	return "Neighbor"
   132  }
   133  
   134  // Priority of neighbor reconciler is higher than pod/service announcements.
   135  // This is important for graceful restart case, where all expected routes are pushed
   136  // into gobgp RIB before neighbors are added. So, gobgp can send out all prefixes
   137  // within initial update message exchange with neighbors before sending EOR marker.
   138  func (r *NeighborReconciler) Priority() int {
   139  	return 60
   140  }
   141  
   142  func (r *NeighborReconciler) Init(_ *instance.BGPInstance) error {
   143  	return nil
   144  }
   145  
   146  func (r *NeighborReconciler) Cleanup(_ *instance.BGPInstance) {}
   147  
   148  func (r *NeighborReconciler) Reconcile(ctx context.Context, p ReconcileParams) error {
   149  	if p.DesiredConfig == nil {
   150  		return fmt.Errorf("attempted neighbor reconciliation with nil CiliumBGPNodeInstance")
   151  	}
   152  	if p.BGPInstance == nil {
   153  		return fmt.Errorf("attempted neighbor reconciliation with nil BGPInstance")
   154  	}
   155  
   156  	var (
   157  		l = r.Logger.WithFields(logrus.Fields{
   158  			types.InstanceLogField: p.DesiredConfig.Name,
   159  		})
   160  
   161  		toCreate []*PeerData
   162  		toRemove []*PeerData
   163  		toUpdate []*PeerData
   164  		curNeigh []*PeerData = nil
   165  	)
   166  	newNeigh := p.DesiredConfig.Peers
   167  
   168  	l.Debug("Begin reconciling peers")
   169  
   170  	// get current configured peers
   171  	curInstance := r.getMetadata(p.BGPInstance)
   172  	if curInstance != nil {
   173  		curNeigh = curInstance[p.DesiredConfig.Name]
   174  	}
   175  
   176  	type member struct {
   177  		new *PeerData
   178  		cur *PeerData
   179  	}
   180  
   181  	nset := map[string]*member{}
   182  
   183  	for i, n := range newNeigh {
   184  		// validate that peer has ASN and address. In current implementation these fields are
   185  		// mandatory for a peer. Eventually we will relax this restriction with implementation
   186  		// of BGP unnumbered.
   187  		if n.PeerASN == nil {
   188  			return fmt.Errorf("peer %s does not have a PeerASN", n.Name)
   189  		}
   190  
   191  		if n.PeerAddress == nil {
   192  			return fmt.Errorf("peer %s does not have a PeerAddress", n.Name)
   193  		}
   194  
   195  		var (
   196  			key = r.neighborID(&n)
   197  			h   *member
   198  			ok  bool
   199  		)
   200  
   201  		config, exists, err := r.getPeerConfig(n.PeerConfigRef)
   202  		if err != nil {
   203  			return err
   204  		}
   205  		if !exists {
   206  			continue // configured peer config does not exist, skip
   207  		}
   208  
   209  		passwd, err := r.getPeerPassword(p.DesiredConfig.Name, n.Name, config)
   210  		if err != nil {
   211  			return err
   212  		}
   213  
   214  		if h, ok = nset[key]; !ok {
   215  			nset[key] = &member{
   216  				new: &PeerData{
   217  					Peer:     &newNeigh[i],
   218  					Config:   config,
   219  					Password: passwd,
   220  				},
   221  			}
   222  			continue
   223  		}
   224  		h.new = &PeerData{
   225  			Peer:     &newNeigh[i],
   226  			Config:   config,
   227  			Password: passwd,
   228  		}
   229  	}
   230  
   231  	for i, n := range curNeigh {
   232  		var (
   233  			key = r.neighborID(n.Peer)
   234  			h   *member
   235  			ok  bool
   236  		)
   237  
   238  		if h, ok = nset[key]; !ok {
   239  			nset[key] = &member{
   240  				cur: curNeigh[i],
   241  			}
   242  			continue
   243  		}
   244  		h.cur = curNeigh[i]
   245  	}
   246  
   247  	for _, m := range nset {
   248  		// present in new neighbors (set new) but not in current neighbors (set cur)
   249  		if m.new != nil && m.cur == nil {
   250  			toCreate = append(toCreate, m.new)
   251  		}
   252  		// present in current neighbors (set cur) but not in new neighbors (set new)
   253  		if m.cur != nil && m.new == nil {
   254  			toRemove = append(toRemove, m.cur)
   255  		}
   256  		// present in both new neighbors (set new) and current neighbors (set cur), update if they are not equal
   257  		if m.cur != nil && m.new != nil {
   258  			if !m.cur.DeepEqual(m.new) {
   259  				toUpdate = append(toUpdate, m.new)
   260  			}
   261  		}
   262  	}
   263  
   264  	if len(toCreate) > 0 || len(toRemove) > 0 || len(toUpdate) > 0 {
   265  		l.Info("Reconciling peers for instance")
   266  	} else {
   267  		l.Debug("No peer changes necessary")
   268  	}
   269  
   270  	// remove neighbors
   271  	for _, n := range toRemove {
   272  		l.WithField(types.PeerLogField, n.Peer.Name).Info("Removing peer")
   273  
   274  		if err := p.BGPInstance.Router.RemoveNeighbor(ctx, types.NeighborRequest{
   275  			Peer: n.Peer,
   276  		}); err != nil {
   277  			return fmt.Errorf("failed to remove neigbhor %s from instance %s: %w", n.Peer.Name, p.DesiredConfig.Name, err)
   278  		}
   279  		// update metadata
   280  		r.deleteMetadata(p.BGPInstance, p.DesiredConfig.Name, n)
   281  	}
   282  
   283  	// update neighbors
   284  	for _, n := range toUpdate {
   285  		l.WithField(types.PeerLogField, n.Peer.Name).Info("Updating peer")
   286  
   287  		if err := p.BGPInstance.Router.UpdateNeighbor(ctx, types.NeighborRequest{
   288  			Peer:       n.Peer,
   289  			PeerConfig: n.Config,
   290  			Password:   n.Password,
   291  		}); err != nil {
   292  			return fmt.Errorf("failed to update neigbhor %s in instance %s: %w", n.Peer.Name, p.DesiredConfig.Name, err)
   293  		}
   294  		// update metadata
   295  		r.upsertMetadata(p.BGPInstance, p.DesiredConfig.Name, n)
   296  	}
   297  
   298  	// create new neighbors
   299  	for _, n := range toCreate {
   300  		l.WithField(types.PeerLogField, n.Peer.Name).Info("Adding peer")
   301  
   302  		if err := p.BGPInstance.Router.AddNeighbor(ctx, types.NeighborRequest{
   303  			Peer:       n.Peer,
   304  			PeerConfig: n.Config,
   305  			Password:   n.Password,
   306  		}); err != nil {
   307  			return fmt.Errorf("failed to add neigbhor %s in instance %s: %w", n.Peer.Name, p.DesiredConfig.Name, err)
   308  		}
   309  		// update metadata
   310  		r.upsertMetadata(p.BGPInstance, p.DesiredConfig.Name, n)
   311  	}
   312  
   313  	l.Debug("Done reconciling peers")
   314  	return nil
   315  }
   316  
   317  // getPeerConfig returns the CiliumBGPPeerConfigSpec for the given peerConfig.
   318  // If peerConfig is not specified, returns the default config.
   319  // If the referenced peerConfig does not exist, exists returns false.
   320  func (r *NeighborReconciler) getPeerConfig(peerConfig *v2alpha1.PeerConfigReference) (conf *v2alpha1.CiliumBGPPeerConfigSpec, exists bool, err error) {
   321  	if peerConfig == nil || peerConfig.Name == "" {
   322  		// if peer config is not specified, return default config
   323  		conf = &v2alpha1.CiliumBGPPeerConfigSpec{}
   324  		conf.SetDefaults()
   325  		return conf, true, nil
   326  	}
   327  
   328  	config, exists, err := r.PeerConfig.GetByKey(resource.Key{Name: peerConfig.Name})
   329  	if err != nil || !exists {
   330  		return nil, exists, err
   331  	}
   332  
   333  	conf = &config.Spec
   334  	conf.SetDefaults()
   335  	return conf, true, nil
   336  }
   337  
   338  func (r *NeighborReconciler) getPeerPassword(instanceName, peerName string, config *v2alpha1.CiliumBGPPeerConfigSpec) (string, error) {
   339  	if config == nil {
   340  		return "", nil
   341  	}
   342  
   343  	l := r.Logger.WithFields(logrus.Fields{
   344  		types.InstanceLogField: instanceName,
   345  		types.PeerLogField:     peerName,
   346  	})
   347  
   348  	if config.AuthSecretRef != nil {
   349  		secretRef := *config.AuthSecretRef
   350  
   351  		secret, ok, err := r.fetchSecret(secretRef)
   352  		if err != nil {
   353  			return "", fmt.Errorf("failed to fetch secret %q: %w", secretRef, err)
   354  		}
   355  		if !ok {
   356  			return "", nil
   357  		}
   358  		tcpPassword := string(secret["password"])
   359  		if tcpPassword == "" {
   360  			return "", fmt.Errorf("failed to fetch secret %q: missing password key", secretRef)
   361  		}
   362  		l.Debugf("Using TCP password from secret %q", secretRef)
   363  		return tcpPassword, nil
   364  	}
   365  	return "", nil
   366  }
   367  
   368  func (r *NeighborReconciler) fetchSecret(name string) (map[string][]byte, bool, error) {
   369  	if r.SecretStore == nil {
   370  		return nil, false, fmt.Errorf("SecretsNamespace not configured")
   371  	}
   372  	item, ok, err := r.SecretStore.GetByKey(resource.Key{Namespace: r.DaemonConfig.BGPSecretsNamespace, Name: name})
   373  	if err != nil || !ok {
   374  		return nil, ok, err
   375  	}
   376  	result := map[string][]byte{}
   377  	for k, v := range item.Data {
   378  		result[k] = []byte(v)
   379  	}
   380  	return result, true, nil
   381  }
   382  
   383  func GetPeerAddressFromConfig(conf *v2alpha1.CiliumBGPNodeInstance, peerName string) (netip.Addr, error) {
   384  	if conf == nil {
   385  		return netip.Addr{}, fmt.Errorf("passed instance is nil")
   386  	}
   387  
   388  	for _, peer := range conf.Peers {
   389  		if peer.Name == peerName {
   390  			if peer.PeerAddress != nil {
   391  				return netip.ParseAddr(*peer.PeerAddress)
   392  			} else {
   393  				return netip.Addr{}, fmt.Errorf("peer %s does not have a PeerAddress", peerName)
   394  			}
   395  		}
   396  	}
   397  	return netip.Addr{}, fmt.Errorf("peer %s not found in instance %s", peerName, conf.Name)
   398  }
   399  
   400  func (r *NeighborReconciler) neighborID(n *v2alpha1.CiliumBGPNodePeer) string {
   401  	return fmt.Sprintf("%s%s%d", n.Name, *n.PeerAddress, *n.PeerASN)
   402  }