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 }