@@ -243,38 +243,61 @@ func (r *BGPConfigurationReconciler) SetupWithManager(ctx context.Context, mgr c
243243 // Skip if
244244 // * no NAD annotation was configured on old object OR
245245 // * no NAD annotation was configured on new object AND
246+ // * no predictableip label was configured on old object OR
247+ // * no predictableip label was configured on new object AND
246248 // * the resourceVersion has not changed
247249
248- oldConfigured := true
250+ oldNADConfigured := true
249251 if val , ok := e .ObjectOld .GetAnnotations ()[k8s_networkv1 .NetworkAttachmentAnnot ]; ! ok || len (val ) == 0 {
250- oldConfigured = false
252+ oldNADConfigured = false
251253 }
252- newConfigured := true
254+ newNADConfigured := true
253255 if val , ok := e .ObjectNew .GetAnnotations ()[k8s_networkv1 .NetworkAttachmentAnnot ]; ! ok || len (val ) == 0 {
254- newConfigured = false
256+ newNADConfigured = false
255257 }
256258
257- return (oldConfigured || newConfigured ) && e .ObjectOld .GetResourceVersion () != e .ObjectNew .GetResourceVersion ()
259+ oldPredictableIPConfigured := true
260+ if val , ok := e .ObjectOld .GetLabels ()["predictableip" ]; ! ok || len (val ) == 0 {
261+ oldPredictableIPConfigured = false
262+ }
263+ newPredictableIPConfigured := true
264+ if val , ok := e .ObjectNew .GetLabels ()["predictableip" ]; ! ok || len (val ) == 0 {
265+ newPredictableIPConfigured = false
266+ }
267+
268+ return (oldNADConfigured || newNADConfigured || oldPredictableIPConfigured || newPredictableIPConfigured ) && e .ObjectOld .GetResourceVersion () != e .ObjectNew .GetResourceVersion ()
258269 },
259270 DeleteFunc : func (e event.DeleteEvent ) bool {
260271 // Skip if
261- // * NAD annotation key is missing
272+ // * NAD annotation key is missing AND predictableip label is missing
262273 // * there is no additional network configured
263- if val , ok := e .Object .GetAnnotations ()[k8s_networkv1 .NetworkAttachmentAnnot ]; ! ok || len (val ) == 0 {
264- return false
274+ nadConfigured := false
275+ if val , ok := e .Object .GetAnnotations ()[k8s_networkv1 .NetworkAttachmentAnnot ]; ok && len (val ) > 0 {
276+ nadConfigured = true
265277 }
266278
267- return true
279+ predictableIPConfigured := false
280+ if val , ok := e .Object .GetLabels ()["predictableip" ]; ok && len (val ) > 0 {
281+ predictableIPConfigured = true
282+ }
283+
284+ return nadConfigured || predictableIPConfigured
268285 },
269286 CreateFunc : func (e event.CreateEvent ) bool {
270287 // Skip if
271- // * NAD annotation key is missing
288+ // * NAD annotation key is missing AND predictableip label is missing
272289 // * there is no additional network configured
273- if val , ok := e .Object .GetAnnotations ()[k8s_networkv1 .NetworkAttachmentAnnot ]; ! ok || len (val ) == 0 {
274- return false
290+ nadConfigured := false
291+ if val , ok := e .Object .GetAnnotations ()[k8s_networkv1 .NetworkAttachmentAnnot ]; ok && len (val ) > 0 {
292+ nadConfigured = true
275293 }
276294
277- return true
295+ predictableIPConfigured := false
296+ if val , ok := e .Object .GetLabels ()["predictableip" ]; ok && len (val ) > 0 {
297+ predictableIPConfigured = true
298+ }
299+
300+ return nadConfigured || predictableIPConfigured
278301 },
279302 }
280303
@@ -315,7 +338,7 @@ func (r *BGPConfigurationReconciler) SetupWithManager(ctx context.Context, mgr c
315338
316339 return ctrl .NewControllerManagedBy (mgr ).
317340 For (& networkv1.BGPConfiguration {}).
318- // Watch pods which have additional networks configured with k8s_networkv1.NetworkAttachmentAnnot annotation in the same namespace
341+ // Watch pods which have additional networks configured with k8s_networkv1.NetworkAttachmentAnnot annotation or predictableip label in the same namespace
319342 Watches (& corev1.Pod {},
320343 podFN ,
321344 builder .WithPredicates (pPod )).
@@ -479,7 +502,7 @@ func (r *BGPConfigurationReconciler) deleteStaleFRRConfigurations(ctx context.Co
479502
480503// getPodNetworkDetails - returns the podDetails for a list of pods in status.phase: Running
481504// where the pod has the multus k8s_networkv1.NetworkAttachmentAnnot annotation
482- // and its value is not '[]'
505+ // and its value is not '[]' OR has a predictableip label
483506func getPodNetworkDetails (
484507 ctx context.Context ,
485508 h * helper.Helper ,
@@ -499,14 +522,24 @@ func getPodNetworkDetails(
499522 if pod .Status .Phase != corev1 .PodRunning {
500523 continue
501524 }
502- if netAttachString , ok := pod .Annotations [k8s_networkv1 .NetworkAttachmentAnnot ]; ok && netAttachString != "[]" {
503- // get the elements from val to validate the status annotation has the right length
504- netAttach := []k8s_networkv1.NetworkSelectionElement {}
505- err := json .Unmarshal ([]byte (netAttachString ), & netAttach )
506- if err != nil {
507- return nil , fmt .Errorf ("failed to decode networks %s: %w" , netAttachString , err )
508- }
509525
526+ // Check for pods with predictableip label
527+ hasPredictableIP := false
528+ predictableIP := ""
529+ if ipValue , ok := pod .Labels ["predictableip" ]; ok && ipValue != "" {
530+ hasPredictableIP = true
531+ predictableIP = ipValue
532+ }
533+
534+ hasNetworkAttachment := false
535+ var netAttachString string
536+ if netAttach , ok := pod .Annotations [k8s_networkv1 .NetworkAttachmentAnnot ]; ok && netAttach != "[]" {
537+ hasNetworkAttachment = true
538+ netAttachString = netAttach
539+ }
540+
541+ // Process pod if it has either network attachments or predictableip label
542+ if hasNetworkAttachment || hasPredictableIP {
510543 // verify the nodeName information is already present in the pod spec, otherwise report an error to reconcile
511544 if pod .Spec .NodeName == "" {
512545 return detailList , fmt .Errorf ("empty spec.nodeName on pod %s" , pod .Name )
@@ -518,55 +551,121 @@ func getPodNetworkDetails(
518551 Node : pod .Spec .NodeName ,
519552 }
520553
521- netsStatus , err := nad .GetNetworkStatusFromAnnotation (pod .Annotations )
522- if err != nil {
523- return detailList , fmt .Errorf ("failed to get netsStatus from pod annoation - %v: %w" , pod .Annotations , err )
524- }
525- // on pod start it can happen that the network status annotation does not yet
526- // reflect all requested networks. return with an error to reconcile if the length
527- // is <= the status. Note: the status also has the pod network
528- if len (netsStatus ) <= len (netAttach ) {
529- return detailList , fmt .Errorf ("metadata.Annotations['k8s.ovn.org/pod-networks'] %s on pod %s, does not match requested networks %s" ,
530- pod .GetAnnotations ()[k8s_networkv1 .NetworkStatusAnnot ], pod .Name , netAttachString )
531- }
554+ var netsStatus []k8s_networkv1.NetworkStatus
555+
556+ // Handle pods with network attachments
557+ if hasNetworkAttachment {
558+ // get the elements from val to validate the status annotation has the right length
559+ netAttach := []k8s_networkv1.NetworkSelectionElement {}
532560
533- netsStatusCopy := make ([]k8s_networkv1.NetworkStatus , len (netsStatus ))
534- copy (netsStatusCopy , netsStatus )
535- // verify there are IP information for all networks in the status, otherwise report an error to reconcile
536- for idx , netStat := range netsStatusCopy {
537- // remove status for the pod interface
538- // it should always be ovn-kubernetes, but if not, remove status for eth0 which is the pod network
539- if netStat .Name == "ovn-kubernetes" || netStat .Interface == "eth0" {
540- removeIndex (netsStatus , idx )
561+ // Validate that the annotation is not empty and contains valid JSON
562+ if netAttachString == "" {
563+ Log .Info (fmt .Sprintf ("Skipping pod %s: network attachment annotation is empty" , pod .Name ))
541564 continue
542565 }
543566
544- // get ipam configuration from NAD
545- nadName := strings .TrimPrefix (netStat .Name , pod .Namespace + "/" )
546- netAtt , err := nad .GetNADWithName (
547- ctx , h , nadName , pod .Namespace )
548- if err != nil {
549- return detailList , err
567+ // Check if the annotation looks like JSON (starts with [ or {) or is a simple string reference
568+ trimmed := strings .TrimSpace (netAttachString )
569+ if ! strings .HasPrefix (trimmed , "[" ) && ! strings .HasPrefix (trimmed , "{" ) && ! strings .HasPrefix (trimmed , "\" " ) {
570+ Log .Info (fmt .Sprintf ("Skipping pod %s: network attachment annotation does not appear to be valid JSON or string reference: %s" , pod .Name , netAttachString ))
571+ continue
572+ }
573+
574+ // Log the raw annotation for debugging
575+ Log .Info (fmt .Sprintf ("Processing pod %s with network attachment annotation: %s" , pod .Name , netAttachString ))
576+
577+ // Handle different annotation formats
578+ var err error
579+ if strings .HasPrefix (trimmed , "\" " ) && strings .HasSuffix (trimmed , "\" " ) {
580+ // Simple string reference like "bgpnet-worker-3"
581+ var nadName string
582+ err = json .Unmarshal ([]byte (netAttachString ), & nadName )
583+ if err != nil {
584+ Log .Error (err , fmt .Sprintf ("Failed to decode network attachment string reference for pod %s. Raw annotation: %s" , pod .Name , netAttachString ))
585+ return nil , fmt .Errorf ("failed to decode network string reference %s for pod %s: %w" , netAttachString , pod .Name , err )
586+ }
587+ // Create a NetworkSelectionElement from the string reference
588+ netAttach = []k8s_networkv1.NetworkSelectionElement {
589+ {
590+ Name : nadName ,
591+ Namespace : pod .Namespace ,
592+ },
593+ }
594+ Log .Info (fmt .Sprintf ("Converted string reference %s to NetworkSelectionElement for pod %s" , nadName , pod .Name ))
595+ } else {
596+ // Full JSON array format
597+ err = json .Unmarshal ([]byte (netAttachString ), & netAttach )
598+ if err != nil {
599+ Log .Error (err , fmt .Sprintf ("Failed to decode network attachment annotation for pod %s. Raw annotation: %s" , pod .Name , netAttachString ))
600+ return nil , fmt .Errorf ("failed to decode networks %s for pod %s: %w" , netAttachString , pod .Name , err )
601+ }
550602 }
551603
552- ipam , err : = nad .GetJSONPathFromConfig ( * netAtt , ".ipam" )
604+ netsStatus , err = nad .GetNetworkStatusFromAnnotation ( pod . Annotations )
553605 if err != nil {
554- return detailList , err
606+ return detailList , fmt .Errorf ("failed to get netsStatus from pod annoation - %v: %w" , pod .Annotations , err )
607+ }
608+ Log .Info (fmt .Sprintf ("Retrieved network statuses for pod %s: %+v" , pod .Name , netsStatus ))
609+ // on pod start it can happen that the network status annotation does not yet
610+ // reflect all requested networks. return with an error to reconcile if the length
611+ // is <= the status. Note: the status also has the pod network
612+ if len (netsStatus ) <= len (netAttach ) {
613+ return detailList , fmt .Errorf ("metadata.Annotations['k8s.ovn.org/pod-networks'] %s on pod %s, does not match requested networks %s" ,
614+ pod .GetAnnotations ()[k8s_networkv1 .NetworkStatusAnnot ], pod .Name , netAttachString )
555615 }
556616
557- // if the NAD has no ipam configured, skip it as there will be no IP
558- if ipam == "{}" {
559- Log .Info (fmt .Sprintf ("removing netsStatus for NAD %s for %s, IPAM configuration is empty: %s" , netAtt .Name , pod .Name , ipam ))
560- removeIndex (netsStatus , idx )
561- continue
617+ netsStatusCopy := make ([]k8s_networkv1.NetworkStatus , len (netsStatus ))
618+ copy (netsStatusCopy , netsStatus )
619+ // verify there are IP information for all networks in the status, otherwise report an error to reconcile
620+ for idx , netStat := range netsStatusCopy {
621+ // remove status for the pod interface
622+ // it should always be ovn-kubernetes, but if not, remove status for eth0 which is the pod network
623+ if netStat .Name == "ovn-kubernetes" || netStat .Interface == "eth0" {
624+ removeIndex (netsStatus , idx )
625+ continue
626+ }
627+
628+ // get ipam configuration from NAD
629+ nadName := strings .TrimPrefix (netStat .Name , pod .Namespace + "/" )
630+ netAtt , err := nad .GetNADWithName (
631+ ctx , h , nadName , pod .Namespace )
632+ if err != nil {
633+ return detailList , err
634+ }
635+
636+ ipam , err := nad .GetJSONPathFromConfig (* netAtt , ".ipam" )
637+ if err != nil {
638+ return detailList , err
639+ }
640+
641+ // if the NAD has no ipam configured, skip it as there will be no IP
642+ if ipam == "{}" {
643+ Log .Info (fmt .Sprintf ("removing netsStatus for NAD %s for %s, IPAM configuration is empty: %s" , netAtt .Name , pod .Name , ipam ))
644+ removeIndex (netsStatus , idx )
645+ continue
646+ }
647+
648+ // verify there is IP information for the network, otherwise report an error to reconcile
649+ if len (netStat .IPs ) == 0 {
650+ return detailList , fmt .Errorf ("no IP information for network %s on pod %s" , netStat .Name , pod .Name )
651+ }
562652 }
653+ }
563654
564- // verify there is IP information for the network, otherwise report an error to reconcile
565- if len (netStat .IPs ) == 0 {
566- return detailList , fmt .Errorf ("no IP information for network %s on pod %s" , netStat .Name , pod .Name )
655+ // Handle pods with predictableip label
656+ if hasPredictableIP {
657+ // Create synthetic NetworkStatus for predictable IP
658+ predictableNetStatus := k8s_networkv1.NetworkStatus {
659+ Name : "predictableip" ,
660+ Interface : "predictableip" ,
661+ IPs : []string {predictableIP },
567662 }
663+ netsStatus = append (netsStatus , predictableNetStatus )
664+ Log .Info (fmt .Sprintf ("Added predictable IP %s for pod %s" , predictableIP , pod .Name ))
568665 }
569666
667+ // Log all network statuses for debugging
668+ Log .Info (fmt .Sprintf ("Pod %s final network statuses: %+v" , pod .Name , netsStatus ))
570669 detail .NetworkStatus = netsStatus
571670
572671 detailList = append (detailList , detail )
@@ -594,6 +693,7 @@ func (r *BGPConfigurationReconciler) createOrPatchFRRConfiguration(
594693 Log .Info ("Reconciling createOrUpdateFRRConfiguration" )
595694
596695 podPrefixes := bgp .GetFRRPodPrefixes (podDtl .NetworkStatus )
696+ Log .Info (fmt .Sprintf ("Generated prefixes for pod %s: %v" , podDtl .Name , podPrefixes ))
597697
598698 nodeFRRCfg := nodeFRRCfgs [podDtl .Node ]
599699
0 commit comments