github.com/cilium/cilium@v1.16.2/operator/pkg/bgpv2/cluster.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package bgpv2
     5  
     6  import (
     7  	"context"
     8  	"errors"
     9  	"fmt"
    10  
    11  	"github.com/sirupsen/logrus"
    12  	k8s_errors "k8s.io/apimachinery/pkg/api/errors"
    13  	meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    14  	"k8s.io/apimachinery/pkg/util/sets"
    15  
    16  	cilium_api_v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
    17  	"github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1"
    18  	"github.com/cilium/cilium/pkg/k8s/resource"
    19  	slim_labels "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/labels"
    20  	slim_meta_v1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1"
    21  )
    22  
    23  func (b *BGPResourceManager) reconcileBGPClusterConfigs(ctx context.Context) error {
    24  	var err error
    25  	for _, config := range b.clusterConfigStore.List() {
    26  		rcErr := b.reconcileBGPClusterConfig(ctx, config)
    27  		if rcErr != nil {
    28  			err = errors.Join(err, rcErr)
    29  		}
    30  	}
    31  	return err
    32  }
    33  
    34  func (b *BGPResourceManager) reconcileBGPClusterConfig(ctx context.Context, config *v2alpha1.CiliumBGPClusterConfig) error {
    35  	// get nodes which match node selector for given cluster config
    36  	matchingNodes, err := b.getMatchingNodes(config.Spec.NodeSelector, config.Name)
    37  	if err != nil {
    38  		return err
    39  	}
    40  
    41  	// update node configs for matched nodes
    42  	for nodeRef := range matchingNodes {
    43  		upsertErr := b.upsertNodeConfig(ctx, config, nodeRef)
    44  		if upsertErr != nil {
    45  			err = errors.Join(err, upsertErr)
    46  		}
    47  	}
    48  
    49  	// delete node configs for this cluster that are not in the matching nodes
    50  	dErr := b.deleteStaleNodeConfigs(ctx, matchingNodes, config.Name)
    51  	if dErr != nil {
    52  		err = errors.Join(err, dErr)
    53  	}
    54  
    55  	return err
    56  }
    57  
    58  func (b *BGPResourceManager) upsertNodeConfig(ctx context.Context, config *v2alpha1.CiliumBGPClusterConfig, nodeName string) error {
    59  	prev, exists, err := b.nodeConfigStore.GetByKey(resource.Key{Name: nodeName})
    60  	if err != nil {
    61  		return err
    62  	}
    63  
    64  	// find node overrides for given node
    65  	var overrideInstances []v2alpha1.CiliumBGPNodeConfigInstanceOverride
    66  	overrides := b.nodeConfigOverrideStore.List()
    67  	for _, override := range overrides {
    68  		if override.Name == nodeName {
    69  			overrideInstances = override.Spec.BGPInstances
    70  			break
    71  		}
    72  	}
    73  
    74  	// create new config
    75  	nodeConfig := &v2alpha1.CiliumBGPNodeConfig{
    76  		ObjectMeta: meta_v1.ObjectMeta{
    77  			Name: nodeName,
    78  			OwnerReferences: []meta_v1.OwnerReference{
    79  				{
    80  					APIVersion: v2alpha1.SchemeGroupVersion.String(),
    81  					Kind:       v2alpha1.BGPCCKindDefinition,
    82  					Name:       config.GetName(),
    83  					UID:        config.GetUID(),
    84  				},
    85  			},
    86  		},
    87  		Spec: v2alpha1.CiliumBGPNodeSpec{
    88  			BGPInstances: toNodeBGPInstance(config.Spec.BGPInstances, overrideInstances),
    89  		},
    90  	}
    91  
    92  	switch {
    93  	case exists && prev.Spec.DeepEqual(&nodeConfig.Spec):
    94  		return nil
    95  	case exists:
    96  		// reinitialize spec and status fields
    97  		prev.Spec = nodeConfig.Spec
    98  		_, err = b.nodeConfigClient.Update(ctx, prev, meta_v1.UpdateOptions{})
    99  	default:
   100  		_, err = b.nodeConfigClient.Create(ctx, nodeConfig, meta_v1.CreateOptions{})
   101  		if err != nil && k8s_errors.IsAlreadyExists(err) {
   102  			// local store is not yet updated, but API server has the resource. Get resource from API server
   103  			// to compare spec.
   104  			prev, err = b.nodeConfigClient.Get(ctx, nodeConfig.Name, meta_v1.GetOptions{})
   105  			if err != nil {
   106  				return err
   107  			}
   108  
   109  			// if prev already exist and spec is different, update it
   110  			if !prev.Spec.DeepEqual(&nodeConfig.Spec) {
   111  				prev.Spec = nodeConfig.Spec
   112  				_, err = b.nodeConfigClient.Update(ctx, prev, meta_v1.UpdateOptions{})
   113  			} else {
   114  				// idempotent change, skip
   115  				return nil
   116  			}
   117  		}
   118  	}
   119  
   120  	b.logger.WithFields(logrus.Fields{
   121  		"node config":    nodeConfig.Name,
   122  		"cluster config": config.Name,
   123  	}).Debug("Upserting BGP node config")
   124  
   125  	return err
   126  }
   127  
   128  func toNodeBGPInstance(clusterBGPInstances []v2alpha1.CiliumBGPInstance, overrideBGPInstances []v2alpha1.CiliumBGPNodeConfigInstanceOverride) []v2alpha1.CiliumBGPNodeInstance {
   129  	var res []v2alpha1.CiliumBGPNodeInstance
   130  
   131  	for _, clusterBGPInstance := range clusterBGPInstances {
   132  		nodeBGPInstance := v2alpha1.CiliumBGPNodeInstance{
   133  			Name:     clusterBGPInstance.Name,
   134  			LocalASN: clusterBGPInstance.LocalASN,
   135  		}
   136  
   137  		// find BGPResourceManager global override for this instance
   138  		var override v2alpha1.CiliumBGPNodeConfigInstanceOverride
   139  		for _, overrideBGPInstance := range overrideBGPInstances {
   140  			if overrideBGPInstance.Name == clusterBGPInstance.Name {
   141  				nodeBGPInstance.RouterID = overrideBGPInstance.RouterID
   142  				nodeBGPInstance.LocalPort = overrideBGPInstance.LocalPort
   143  				override = overrideBGPInstance
   144  				break
   145  			}
   146  		}
   147  
   148  		for _, clusterBGPInstancePeer := range clusterBGPInstance.Peers {
   149  			nodePeer := v2alpha1.CiliumBGPNodePeer{
   150  				Name:          clusterBGPInstancePeer.Name,
   151  				PeerAddress:   clusterBGPInstancePeer.PeerAddress,
   152  				PeerASN:       clusterBGPInstancePeer.PeerASN,
   153  				PeerConfigRef: clusterBGPInstancePeer.PeerConfigRef,
   154  			}
   155  
   156  			// find BGPResourceManager Peer override for this instance
   157  			for _, overrideBGPPeer := range override.Peers {
   158  				if overrideBGPPeer.Name == clusterBGPInstancePeer.Name {
   159  					nodePeer.LocalAddress = overrideBGPPeer.LocalAddress
   160  					break
   161  				}
   162  			}
   163  
   164  			nodeBGPInstance.Peers = append(nodeBGPInstance.Peers, nodePeer)
   165  		}
   166  
   167  		res = append(res, nodeBGPInstance)
   168  	}
   169  	return res
   170  }
   171  
   172  // getMatchingNodes returns a map of node names that match the given cluster config's node selector.
   173  func (b *BGPResourceManager) getMatchingNodes(nodeSelector *slim_meta_v1.LabelSelector, configName string) (sets.Set[string], error) {
   174  	labelSelector, err := slim_meta_v1.LabelSelectorAsSelector(nodeSelector)
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  
   179  	// find nodes that match the cluster config's node selector
   180  	matchingNodes := sets.New[string]()
   181  
   182  	for _, n := range b.ciliumNodeStore.List() {
   183  		// nil node selector means match all nodes
   184  		if nodeSelector == nil || labelSelector.Matches(slim_labels.Set(n.Labels)) {
   185  			err := b.validNodeSelection(n, configName)
   186  			if err != nil {
   187  				b.logger.WithError(err).Errorf("skipping node %s", n.Name)
   188  				continue
   189  			}
   190  			matchingNodes.Insert(n.Name)
   191  		}
   192  	}
   193  
   194  	return matchingNodes, nil
   195  }
   196  
   197  // deleteStaleNodeConfigs deletes node configs that are not in the expected list for given cluster.
   198  // TODO there might be a race where stale node configs are not detected and deleted. Check issue #30320 for more details.
   199  func (b *BGPResourceManager) deleteStaleNodeConfigs(ctx context.Context, expectedNodes sets.Set[string], clusterRef string) error {
   200  	var err error
   201  	for _, existingNode := range b.nodeConfigStore.List() {
   202  		if expectedNodes.Has(existingNode.Name) || !IsOwner(existingNode.GetOwnerReferences(), clusterRef) {
   203  			continue
   204  		}
   205  
   206  		dErr := b.nodeConfigClient.Delete(ctx, existingNode.Name, meta_v1.DeleteOptions{})
   207  		if dErr != nil && k8s_errors.IsNotFound(dErr) {
   208  			continue
   209  		} else if dErr != nil {
   210  			err = errors.Join(err, dErr)
   211  		} else {
   212  			b.logger.WithFields(logrus.Fields{
   213  				"node config":    existingNode.Name,
   214  				"cluster config": clusterRef,
   215  			}).Debug("Deleting BGP node config")
   216  		}
   217  	}
   218  	return err
   219  }
   220  
   221  // validNodeSelection checks if the node is already present in another cluster config.
   222  func (b *BGPResourceManager) validNodeSelection(node *cilium_api_v2.CiliumNode, expectedOwnerName string) error {
   223  	existingBGPNodeConfig, exists, err := b.nodeConfigStore.GetByKey(resource.Key{Name: node.Name})
   224  	if err != nil {
   225  		return err
   226  	}
   227  
   228  	if exists && !IsOwner(existingBGPNodeConfig.GetOwnerReferences(), expectedOwnerName) {
   229  		return fmt.Errorf("BGPResourceManager node config %s already exist", existingBGPNodeConfig.Name)
   230  	}
   231  
   232  	return nil
   233  }
   234  
   235  // IsOwner checks if the expected is present in owners list.
   236  func IsOwner(owners []meta_v1.OwnerReference, expected string) bool {
   237  	for _, owner := range owners {
   238  		if owner.Name == expected {
   239  			return true
   240  		}
   241  	}
   242  	return false
   243  }