github.com/cilium/cilium@v1.16.2/pkg/bgpv1/manager/reconcilerv2/pod_ip_pool.go (about) 1 // SPDX-License-Identifier: Apache-2.0 2 // Copyright Authors of Cilium 3 4 package reconcilerv2 5 6 import ( 7 "context" 8 "errors" 9 "fmt" 10 "maps" 11 "net/netip" 12 13 "github.com/cilium/hive/cell" 14 "github.com/sirupsen/logrus" 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 v2alpha1 "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-v2"` 35 } 36 37 type PodIPPoolReconcilerIn struct { 38 cell.In 39 40 Logger logrus.FieldLogger 41 PeerAdvert *CiliumPeerAdvertisement 42 PoolStore store.BGPCPResourceStore[*v2alpha1.CiliumPodIPPool] 43 } 44 45 type PodIPPoolReconciler struct { 46 logger logrus.FieldLogger 47 peerAdvert *CiliumPeerAdvertisement 48 poolStore store.BGPCPResourceStore[*v2alpha1.CiliumPodIPPool] 49 } 50 51 // PodIPPoolReconcilerMetadata holds any announced pod ip pool CIDRs keyed by pool name of the backing CiliumPodIPPool. 52 type PodIPPoolReconcilerMetadata struct { 53 PoolAFPaths ResourceAFPathsMap 54 PoolRoutePolicies ResourceRoutePolicyMap 55 } 56 57 func NewPodIPPoolReconciler(in PodIPPoolReconcilerIn) PodIPPoolReconcilerOut { 58 if in.PoolStore == nil { 59 return PodIPPoolReconcilerOut{} 60 } 61 62 return PodIPPoolReconcilerOut{ 63 Reconciler: &PodIPPoolReconciler{ 64 logger: in.Logger.WithField(types.ReconcilerLogField, "PodIPPool"), 65 peerAdvert: in.PeerAdvert, 66 poolStore: in.PoolStore, 67 }, 68 } 69 } 70 71 func (r *PodIPPoolReconciler) Name() string { 72 return "PodIPPool" 73 } 74 75 func (r *PodIPPoolReconciler) Priority() int { 76 return 50 77 } 78 79 func (r *PodIPPoolReconciler) Init(_ *instance.BGPInstance) error { 80 return nil 81 } 82 83 func (r *PodIPPoolReconciler) Cleanup(_ *instance.BGPInstance) {} 84 85 func (r *PodIPPoolReconciler) Reconcile(ctx context.Context, p ReconcileParams) error { 86 if p.DesiredConfig == nil { 87 return fmt.Errorf("BUG: PodIPPoolReconciler reconciler called with nil CiliumBGPNodeConfig") 88 } 89 90 if p.CiliumNode == nil { 91 return fmt.Errorf("BUG: PodIPPoolReconciler reconciler called with nil CiliumNode") 92 } 93 94 lp := r.populateLocalPools(p.CiliumNode) 95 96 desiredPeerAdverts, err := r.peerAdvert.GetConfiguredAdvertisements(p.DesiredConfig, v2alpha1.BGPCiliumPodIPPoolAdvert) 97 if err != nil { 98 return err 99 } 100 101 err = r.reconcileRoutePolicies(ctx, p, desiredPeerAdverts, lp) 102 if err != nil { 103 return err 104 } 105 106 return r.reconcilePaths(ctx, p, desiredPeerAdverts, lp) 107 } 108 109 func (r *PodIPPoolReconciler) reconcilePaths(ctx context.Context, p ReconcileParams, desiredPeerAdverts PeerAdvertisements, lp map[string][]netip.Prefix) error { 110 poolsAFPaths, err := r.getDesiredPoolAFPaths(p, desiredPeerAdverts, lp) 111 if err != nil { 112 return err 113 } 114 115 metadata := r.getMetadata(p.BGPInstance) 116 for poolKey, desiredPoolAFPaths := range poolsAFPaths { 117 currentPoolAFPaths, exists := metadata.PoolAFPaths[poolKey] 118 if !exists && len(desiredPoolAFPaths) == 0 { 119 // No paths to reconcile for this pool. 120 continue 121 } 122 123 updatedPoolAFPaths, rErr := ReconcileAFPaths(&ReconcileAFPathsParams{ 124 Logger: r.logger.WithFields( 125 logrus.Fields{ 126 types.InstanceLogField: p.DesiredConfig.Name, 127 types.PodIPPoolLogField: poolKey, 128 }), 129 Ctx: ctx, 130 Router: p.BGPInstance.Router, 131 DesiredPaths: desiredPoolAFPaths, 132 CurrentPaths: currentPoolAFPaths, 133 }) 134 135 if rErr == nil && len(desiredPoolAFPaths) == 0 { 136 // No paths left for this pool. 137 delete(metadata.PoolAFPaths, poolKey) 138 } else { 139 metadata.PoolAFPaths[poolKey] = updatedPoolAFPaths 140 } 141 err = errors.Join(err, rErr) 142 } 143 r.setMetadata(p.BGPInstance, metadata) 144 return err 145 } 146 147 func (r *PodIPPoolReconciler) getDesiredPoolAFPaths(p ReconcileParams, desiredFamilyAdverts PeerAdvertisements, lp map[string][]netip.Prefix) (ResourceAFPathsMap, error) { 148 desiredPoolAFPaths := make(ResourceAFPathsMap) 149 150 metadata := r.getMetadata(p.BGPInstance) 151 152 // check if any pool is deleted 153 for poolKey := range metadata.PoolAFPaths { 154 _, exists, err := r.poolStore.GetByKey(poolKey) 155 if err != nil { 156 return nil, err 157 } 158 159 if !exists { 160 // pool is deleted, mark it for removal 161 desiredPoolAFPaths[poolKey] = nil 162 } 163 } 164 165 pools, err := r.poolStore.List() 166 if err != nil { 167 return nil, err 168 } 169 170 for _, pool := range pools { 171 desiredPaths, err := r.getDesiredAFPaths(pool, desiredFamilyAdverts, lp) 172 if err != nil { 173 return nil, err 174 } 175 176 poolKey := resource.Key{ 177 Name: pool.Name, 178 Namespace: pool.Namespace, 179 } 180 181 desiredPoolAFPaths[poolKey] = desiredPaths 182 } 183 return desiredPoolAFPaths, nil 184 } 185 186 func (r *PodIPPoolReconciler) reconcileRoutePolicies(ctx context.Context, p ReconcileParams, desiredPeerAdverts PeerAdvertisements, lp map[string][]netip.Prefix) error { 187 desiredPoolsRPs, err := r.getDesiredPodIPPoolRoutePolicies(p, desiredPeerAdverts, lp) 188 if err != nil { 189 return err 190 } 191 192 metadata := r.getMetadata(p.BGPInstance) 193 for poolKey, desiredRPs := range desiredPoolsRPs { 194 currentRPs, exists := metadata.PoolRoutePolicies[poolKey] 195 if !exists && len(desiredRPs) == 0 { 196 continue 197 } 198 199 updatedRPs, rErr := ReconcileRoutePolicies(&ReconcileRoutePoliciesParams{ 200 Logger: r.logger.WithFields( 201 logrus.Fields{ 202 types.InstanceLogField: p.DesiredConfig.Name, 203 types.PodIPPoolLogField: poolKey, 204 }), 205 Ctx: ctx, 206 Router: p.BGPInstance.Router, 207 DesiredPolicies: desiredRPs, 208 CurrentPolicies: currentRPs, 209 }) 210 211 if rErr == nil && len(desiredRPs) == 0 { 212 delete(metadata.PoolRoutePolicies, poolKey) 213 } else { 214 metadata.PoolRoutePolicies[poolKey] = updatedRPs 215 } 216 217 err = errors.Join(err, rErr) 218 } 219 r.setMetadata(p.BGPInstance, metadata) 220 221 return err 222 } 223 224 func (r *PodIPPoolReconciler) getDesiredPodIPPoolRoutePolicies(p ReconcileParams, desiredPeerAdverts PeerAdvertisements, lp map[string][]netip.Prefix) (ResourceRoutePolicyMap, error) { 225 metadata := r.getMetadata(p.BGPInstance) 226 227 desiredPodIPPoolRoutePolicies := make(ResourceRoutePolicyMap) 228 229 // mark for deleting pool policies 230 for poolKey := range metadata.PoolRoutePolicies { 231 _, exists, err := r.poolStore.GetByKey(poolKey) 232 if err != nil { 233 return nil, err 234 } 235 236 if !exists { 237 // pool is deleted, mark it for removal 238 desiredPodIPPoolRoutePolicies[poolKey] = nil 239 } 240 } 241 242 // get all pools and their route policies 243 pools, err := r.poolStore.List() 244 if err != nil { 245 return nil, err 246 } 247 248 for _, pool := range pools { 249 desiredPoolRoutePolicies, err := r.getPodIPPoolPolicies(p, pool, desiredPeerAdverts, lp) 250 if err != nil { 251 return nil, err 252 } 253 254 key := resource.Key{ 255 Name: pool.Name, 256 Namespace: pool.Namespace, 257 } 258 desiredPodIPPoolRoutePolicies[key] = desiredPoolRoutePolicies 259 } 260 261 return desiredPodIPPoolRoutePolicies, nil 262 } 263 264 func (r *PodIPPoolReconciler) getPodIPPoolPolicies(p ReconcileParams, pool *v2alpha1.CiliumPodIPPool, desiredPeerAdverts PeerAdvertisements, lp map[string][]netip.Prefix) (RoutePolicyMap, error) { 265 desiredRoutePolicies := make(RoutePolicyMap) 266 267 for peer, afAdverts := range desiredPeerAdverts { 268 for family, adverts := range afAdverts { 269 fam := types.ToAgentFamily(family) 270 for _, advert := range adverts { 271 policy, err := r.getPodIPPoolPolicy(p, peer, fam, pool, advert, lp) 272 if err != nil { 273 return nil, err 274 } 275 if policy != nil { 276 desiredRoutePolicies[policy.Name] = policy 277 } 278 } 279 } 280 } 281 282 return desiredRoutePolicies, nil 283 } 284 285 // populateLocalPools returns a map of allocated multi-pool IPAM CIDRs of the local CiliumNode, 286 // keyed by the pool name. 287 func (r *PodIPPoolReconciler) populateLocalPools(localNode *v2api.CiliumNode) map[string][]netip.Prefix { 288 if localNode == nil { 289 return nil 290 } 291 292 lp := make(map[string][]netip.Prefix) 293 for _, pool := range localNode.Spec.IPAM.Pools.Allocated { 294 var prefixes []netip.Prefix 295 for _, cidr := range pool.CIDRs { 296 if p, err := cidr.ToPrefix(); err == nil { 297 prefixes = append(prefixes, *p) 298 } else { 299 r.logger.WithField(types.PrefixLogField, cidr).WithError(err).Error("invalid IPAM pool CIDR") 300 } 301 } 302 lp[pool.Pool] = prefixes 303 } 304 305 return lp 306 } 307 308 func (r *PodIPPoolReconciler) getDesiredAFPaths(pool *v2alpha1.CiliumPodIPPool, desiredPeerAdverts PeerAdvertisements, lp map[string][]netip.Prefix) (AFPathsMap, error) { 309 // Calculate desired paths per address family, collapsing per-peer advertisements into per-family advertisements. 310 desiredFamilyAdverts := make(AFPathsMap) 311 312 for _, peerFamilyAdverts := range desiredPeerAdverts { 313 for family, familyAdverts := range peerFamilyAdverts { 314 agentFamily := types.ToAgentFamily(family) 315 316 for _, advert := range familyAdverts { 317 // sanity check advertisement type 318 if advert.AdvertisementType != v2alpha1.BGPCiliumPodIPPoolAdvert { 319 r.logger.WithField(types.AdvertTypeLogField, advert.AdvertisementType).Error("BUG: unexpected advertisement type") 320 continue 321 } 322 323 // check if the pool selector matches the advertisement 324 poolSelector, err := slim_metav1.LabelSelectorAsSelector(advert.Selector) 325 if err != nil { 326 return nil, fmt.Errorf("failed to convert label selector: %w", err) 327 } 328 329 // Ignore non matching pool. 330 if !poolSelector.Matches(podIPPoolLabelSet(pool)) { 331 continue 332 } 333 334 if prefixes, exists := lp[pool.Name]; exists { 335 // on the local node we have this pool configured. 336 // add the prefixes to the desiredPaths. 337 for _, prefix := range prefixes { 338 path := types.NewPathForPrefix(prefix) 339 path.Family = agentFamily 340 341 // we only add path corresponding to the family of the prefix. 342 if agentFamily.Afi == types.AfiIPv4 && prefix.Addr().Is4() { 343 addPathToAFPathsMap(desiredFamilyAdverts, agentFamily, path) 344 } 345 if agentFamily.Afi == types.AfiIPv6 && prefix.Addr().Is6() { 346 addPathToAFPathsMap(desiredFamilyAdverts, agentFamily, path) 347 } 348 } 349 } 350 } 351 } 352 } 353 354 return desiredFamilyAdverts, nil 355 } 356 357 func (r *PodIPPoolReconciler) getPodIPPoolPolicy(p ReconcileParams, peer string, family types.Family, pool *v2alpha1.CiliumPodIPPool, advert v2alpha1.BGPAdvertisement, lp map[string][]netip.Prefix) (*types.RoutePolicy, error) { 358 // get the peer address 359 peerAddr, err := GetPeerAddressFromConfig(p.DesiredConfig, peer) 360 if err != nil { 361 return nil, fmt.Errorf("failed to get peer address: %w", err) 362 } 363 364 // check if the pool selector matches the advertisement 365 poolSelector, err := slim_metav1.LabelSelectorAsSelector(advert.Selector) 366 if err != nil { 367 return nil, fmt.Errorf("failed to convert label selector: %w", err) 368 } 369 370 // Ignore non matching pool. 371 if !poolSelector.Matches(podIPPoolLabelSet(pool)) { 372 return nil, nil 373 } 374 375 // only include pool cidrs that have been allocated to the local node. 376 prefixes, exists := lp[pool.Name] 377 if !exists { 378 return nil, nil 379 } 380 381 var v4Prefixes, v6Prefixes types.PolicyPrefixMatchList 382 383 for _, prefix := range prefixes { 384 if family.Afi == types.AfiIPv4 && prefix.Addr().Is4() { 385 prefixLen := int(pool.Spec.IPv4.MaskSize) 386 v4Prefixes = append(v4Prefixes, &types.RoutePolicyPrefixMatch{ 387 CIDR: prefix, 388 PrefixLenMin: prefixLen, 389 PrefixLenMax: prefixLen, 390 }) 391 } 392 393 if family.Afi == types.AfiIPv6 && prefix.Addr().Is6() { 394 prefixLen := int(pool.Spec.IPv6.MaskSize) 395 v6Prefixes = append(v6Prefixes, &types.RoutePolicyPrefixMatch{ 396 CIDR: prefix, 397 PrefixLenMin: prefixLen, 398 PrefixLenMax: prefixLen, 399 }) 400 } 401 } 402 403 // if no prefixes are found for the pool, return nil 404 if len(v4Prefixes) == 0 && len(v6Prefixes) == 0 { 405 return nil, nil 406 } 407 408 policyName := PolicyName(peer, family.Afi.String(), advert.AdvertisementType, pool.Name) 409 return CreatePolicy(policyName, peerAddr, v4Prefixes, v6Prefixes, advert) 410 } 411 412 func podIPPoolLabelSet(pool *v2alpha1.CiliumPodIPPool) labels.Labels { 413 poolLabels := maps.Clone(pool.Labels) 414 if poolLabels == nil { 415 poolLabels = make(map[string]string) 416 } 417 poolLabels[podIPPoolNameLabel] = pool.Name 418 poolLabels[podIPPoolNamespaceLabel] = pool.Namespace 419 return labels.Set(poolLabels) 420 } 421 422 func (r *PodIPPoolReconciler) getMetadata(i *instance.BGPInstance) PodIPPoolReconcilerMetadata { 423 if _, found := i.Metadata[r.Name()]; !found { 424 i.Metadata[r.Name()] = PodIPPoolReconcilerMetadata{ 425 PoolAFPaths: make(ResourceAFPathsMap), 426 PoolRoutePolicies: make(ResourceRoutePolicyMap), 427 } 428 } 429 return i.Metadata[r.Name()].(PodIPPoolReconcilerMetadata) 430 } 431 432 func (r *PodIPPoolReconciler) setMetadata(i *instance.BGPInstance, metadata PodIPPoolReconcilerMetadata) { 433 i.Metadata[r.Name()] = metadata 434 }