github.com/cilium/cilium@v1.16.2/pkg/bgpv1/manager/reconciler/pod_ip_pool.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package reconciler
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"net/netip"
    10  	"slices"
    11  
    12  	"github.com/cilium/hive/cell"
    13  	"github.com/sirupsen/logrus"
    14  	"golang.org/x/exp/maps"
    15  
    16  	"github.com/cilium/cilium/pkg/bgpv1/manager/instance"
    17  	"github.com/cilium/cilium/pkg/bgpv1/manager/store"
    18  	"github.com/cilium/cilium/pkg/bgpv1/types"
    19  	v2api "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
    20  	v2alpha1api "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2alpha1"
    21  	"github.com/cilium/cilium/pkg/k8s/resource"
    22  	"github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/labels"
    23  	slim_metav1 "github.com/cilium/cilium/pkg/k8s/slim/k8s/apis/meta/v1"
    24  )
    25  
    26  const (
    27  	podIPPoolNameLabel      = "io.cilium.podippool.name"
    28  	podIPPoolNamespaceLabel = "io.cilium.podippool.namespace"
    29  )
    30  
    31  type PodIPPoolReconcilerOut struct {
    32  	cell.Out
    33  
    34  	Reconciler ConfigReconciler `group:"bgp-config-reconciler"`
    35  }
    36  
    37  type PodIPPoolReconciler struct {
    38  	poolStore store.BGPCPResourceStore[*v2alpha1api.CiliumPodIPPool]
    39  }
    40  
    41  // PodIPPoolReconcilerMetadata holds any announced pod ip pool CIDRs keyed by pool name of the backing CiliumPodIPPool.
    42  type PodIPPoolReconcilerMetadata map[resource.Key][]*types.Path
    43  
    44  func NewPodIPPoolReconciler(poolStore store.BGPCPResourceStore[*v2alpha1api.CiliumPodIPPool]) PodIPPoolReconcilerOut {
    45  	if poolStore == nil {
    46  		return PodIPPoolReconcilerOut{}
    47  	}
    48  
    49  	return PodIPPoolReconcilerOut{
    50  		Reconciler: &PodIPPoolReconciler{
    51  			poolStore: poolStore,
    52  		},
    53  	}
    54  }
    55  
    56  func (r *PodIPPoolReconciler) Name() string {
    57  	return "PodIPPool"
    58  }
    59  
    60  func (r *PodIPPoolReconciler) Priority() int {
    61  	return 50
    62  }
    63  
    64  func (r *PodIPPoolReconciler) Init(_ *instance.ServerWithConfig) error {
    65  	return nil
    66  }
    67  
    68  func (r *PodIPPoolReconciler) Cleanup(_ *instance.ServerWithConfig) {}
    69  
    70  func (r *PodIPPoolReconciler) Reconcile(ctx context.Context, p ReconcileParams) error {
    71  	lp := r.populateLocalPools(p.CiliumNode)
    72  
    73  	if err := r.fullReconciliation(ctx, p.CurrentServer, p.DesiredConfig, lp); err != nil {
    74  		return fmt.Errorf("full reconciliation failed: %w", err)
    75  	}
    76  
    77  	return nil
    78  }
    79  
    80  func (r *PodIPPoolReconciler) getMetadata(sc *instance.ServerWithConfig) PodIPPoolReconcilerMetadata {
    81  	if _, found := sc.ReconcilerMetadata[r.Name()]; !found {
    82  		sc.ReconcilerMetadata[r.Name()] = make(PodIPPoolReconcilerMetadata)
    83  	}
    84  	return sc.ReconcilerMetadata[r.Name()].(PodIPPoolReconcilerMetadata)
    85  }
    86  
    87  // populateLocalPools returns a map of allocated multi-pool IPAM CIDRs of the local CiliumNode,
    88  // keyed by the pool name.
    89  func (r *PodIPPoolReconciler) populateLocalPools(localNode *v2api.CiliumNode) map[string][]netip.Prefix {
    90  	var (
    91  		l = log.WithFields(
    92  			logrus.Fields{
    93  				"component": "PodIPPoolReconciler",
    94  			},
    95  		)
    96  	)
    97  
    98  	if localNode == nil {
    99  		return nil
   100  	}
   101  
   102  	lp := make(map[string][]netip.Prefix)
   103  	for _, pool := range localNode.Spec.IPAM.Pools.Allocated {
   104  		var prefixes []netip.Prefix
   105  		for _, cidr := range pool.CIDRs {
   106  			if p, err := cidr.ToPrefix(); err == nil {
   107  				prefixes = append(prefixes, *p)
   108  			} else {
   109  				l.Errorf("invalid ipam pool cidr %v: %v", cidr, err)
   110  			}
   111  		}
   112  		lp[pool.Pool] = prefixes
   113  	}
   114  
   115  	return lp
   116  }
   117  
   118  // fullReconciliation reconciles all pod ip pools.
   119  func (r *PodIPPoolReconciler) fullReconciliation(ctx context.Context,
   120  	sc *instance.ServerWithConfig,
   121  	newc *v2alpha1api.CiliumBGPVirtualRouter,
   122  	localPools map[string][]netip.Prefix) error {
   123  	podIPPoolAnnouncements := r.getMetadata(sc)
   124  	// Loop over all existing announcements, delete announcements for pod ip pools that no longer exist.
   125  	for poolKey := range podIPPoolAnnouncements {
   126  		_, found, err := r.poolStore.GetByKey(poolKey)
   127  		if err != nil {
   128  			return fmt.Errorf("failed to get pod ip pool from resource store: %w", err)
   129  		}
   130  		// If the pod ip pool no longer exists, withdraw all associated routes.
   131  		if !found {
   132  			if err := r.withdrawPool(ctx, sc, poolKey); err != nil {
   133  				return fmt.Errorf("failed to withdraw pod ip pool: %w", err)
   134  			}
   135  			continue
   136  		}
   137  	}
   138  
   139  	// Loop over all pod ip pools, reconcile any updates to the pool.
   140  	pools, err := r.poolStore.List()
   141  	if err != nil {
   142  		return fmt.Errorf("failed to list ip pools from store: %w", err)
   143  	}
   144  	for _, pool := range pools {
   145  		if err := r.reconcilePodIPPool(ctx, sc, newc, pool, localPools); err != nil {
   146  			return fmt.Errorf("failed to reconcile pod ip pool: %w", err)
   147  		}
   148  	}
   149  
   150  	return nil
   151  }
   152  
   153  // withdrawPool removes all announcements for the given pod ip pool.
   154  func (r *PodIPPoolReconciler) withdrawPool(ctx context.Context, sc *instance.ServerWithConfig, key resource.Key) error {
   155  	podIPPoolAnnouncements := r.getMetadata(sc)
   156  	advertisements := podIPPoolAnnouncements[key]
   157  	// Loop in reverse order so we can delete without effect to the iteration.
   158  	for i := len(advertisements) - 1; i >= 0; i-- {
   159  		advertisement := advertisements[i]
   160  		if err := sc.Server.WithdrawPath(ctx, types.PathRequest{Path: advertisement}); err != nil {
   161  			// Persist remaining advertisements
   162  			podIPPoolAnnouncements[key] = advertisements
   163  			return fmt.Errorf("failed to withdraw deleted pod ip pool route: %v: %w", advertisement.NLRI, err)
   164  		}
   165  
   166  		// Delete the advertisement after each withdraw in case we error half way through
   167  		advertisements = slices.Delete(advertisements, i, i+1)
   168  	}
   169  
   170  	// If all were withdrawn without error, we can delete the whole pod ip pool from the map
   171  	delete(podIPPoolAnnouncements, key)
   172  
   173  	return nil
   174  }
   175  
   176  // reconcilePodIPPool ensures the CIDRs of the given pool are announced if they are present
   177  // on the local node, adding missing announcements or withdrawing unwanted ones.
   178  func (r *PodIPPoolReconciler) reconcilePodIPPool(ctx context.Context,
   179  	sc *instance.ServerWithConfig,
   180  	newc *v2alpha1api.CiliumBGPVirtualRouter,
   181  	pool *v2alpha1api.CiliumPodIPPool,
   182  	localPools map[string][]netip.Prefix) error {
   183  	podIPPoolAnnouncements := r.getMetadata(sc)
   184  	poolKey := resource.NewKey(pool)
   185  
   186  	desiredRoutes, err := r.poolDesiredRoutes(newc, pool, localPools)
   187  	if err != nil {
   188  		return fmt.Errorf("poolDesiredRoutes(): %w", err)
   189  	}
   190  
   191  	for _, desiredRoute := range desiredRoutes {
   192  		// If this route has already been announced, don't add it again
   193  		if slices.ContainsFunc(podIPPoolAnnouncements[poolKey], func(existing *types.Path) bool {
   194  			return desiredRoute.String() == existing.NLRI.String()
   195  		}) {
   196  			continue
   197  		}
   198  
   199  		// Advertise the new cidr
   200  		advertPathResp, err := sc.Server.AdvertisePath(ctx, types.PathRequest{
   201  			Path: types.NewPathForPrefix(desiredRoute),
   202  		})
   203  		if err != nil {
   204  			return fmt.Errorf("failed to advertise podippool cidr route %v: %w", desiredRoute, err)
   205  		}
   206  		podIPPoolAnnouncements[poolKey] = append(podIPPoolAnnouncements[poolKey], advertPathResp.Path)
   207  	}
   208  
   209  	// Loop over announcements in reverse order so we can delete entries without effecting iteration.
   210  	for i := len(podIPPoolAnnouncements[poolKey]) - 1; i >= 0; i-- {
   211  		announcement := podIPPoolAnnouncements[poolKey][i]
   212  		// If the announcement is within the list of desired routes, don't remove it
   213  		if slices.ContainsFunc(desiredRoutes, func(existing netip.Prefix) bool {
   214  			return existing.String() == announcement.NLRI.String()
   215  		}) {
   216  			continue
   217  		}
   218  
   219  		if err := sc.Server.WithdrawPath(ctx, types.PathRequest{Path: announcement}); err != nil {
   220  			return fmt.Errorf("failed to withdraw podippool cidr route %s: %w", announcement.NLRI, err)
   221  		}
   222  
   223  		// Delete announcement from slice
   224  		podIPPoolAnnouncements[poolKey] = slices.Delete(podIPPoolAnnouncements[poolKey], i, i+1)
   225  	}
   226  
   227  	return nil
   228  }
   229  
   230  // poolDesiredRoutes returns routes that should be announced for the given pool.
   231  func (r *PodIPPoolReconciler) poolDesiredRoutes(
   232  	newc *v2alpha1api.CiliumBGPVirtualRouter,
   233  	pool *v2alpha1api.CiliumPodIPPool,
   234  	localPools map[string][]netip.Prefix) ([]netip.Prefix, error) {
   235  	if newc.PodIPPoolSelector == nil {
   236  		// If the vRouter has no pool selector, there are no desired routes.
   237  		return nil, nil
   238  	}
   239  
   240  	// The vRouter has a pool selector, so determine the desired routes.
   241  	poolSelector, err := slim_metav1.LabelSelectorAsSelector(newc.PodIPPoolSelector)
   242  	if err != nil {
   243  		return nil, fmt.Errorf("failed to convert label selector: %w", err)
   244  	}
   245  
   246  	// Ignore non matching pools.
   247  	if !poolSelector.Matches(podIPPoolLabelSet(pool)) {
   248  		return nil, nil
   249  	}
   250  
   251  	var desiredRoutes []netip.Prefix
   252  	if localCIDRs, ok := localPools[pool.Name]; ok {
   253  		desiredRoutes = append(desiredRoutes, localCIDRs...)
   254  	}
   255  
   256  	return desiredRoutes, nil
   257  }
   258  
   259  func podIPPoolLabelSet(pool *v2alpha1api.CiliumPodIPPool) labels.Labels {
   260  	poolLabels := maps.Clone(pool.Labels)
   261  	if poolLabels == nil {
   262  		poolLabels = make(map[string]string)
   263  	}
   264  	poolLabels[podIPPoolNameLabel] = pool.Name
   265  	poolLabels[podIPPoolNamespaceLabel] = pool.Namespace
   266  	return labels.Set(poolLabels)
   267  }