@@ -58,6 +58,14 @@ const (
5858 // prevents the public IP from being released when the service is deleted.
5959 ServiceAnnotationLoadBalancerKeepIP = "service.beta.kubernetes.io/cloudstack-load-balancer-keep-ip"
6060
61+ // ServiceAnnotationLoadBalancerID stores the CloudStack public IP UUID associated with the load balancer.
62+ // Used for efficient ID-based lookups instead of keyword-based searches.
63+ ServiceAnnotationLoadBalancerID = "service.beta.kubernetes.io/cloudstack-load-balancer-id"
64+
65+ // ServiceAnnotationLoadBalancerNetworkID stores the CloudStack network UUID associated with the load balancer.
66+ // Used together with ServiceAnnotationLoadBalancerID for scoped ID-based lookups.
67+ ServiceAnnotationLoadBalancerNetworkID = "service.beta.kubernetes.io/cloudstack-load-balancer-network-id"
68+
6169 // Used to construct the load balancer name.
6270 servicePrefix = "K8s_svc_"
6371 lbNameFormat = "%s%s_%s_%s"
@@ -83,7 +91,7 @@ func (cs *CSCloud) GetLoadBalancer(ctx context.Context, clusterName string, serv
8391 // Get the load balancer details and existing rules.
8492 name := cs .GetLoadBalancerName (ctx , clusterName , service )
8593 legacyName := cs .getLoadBalancerLegacyName (ctx , clusterName , service )
86- lb , err := cs .getLoadBalancerByName ( name , legacyName )
94+ lb , err := cs .getLoadBalancer ( service , name , legacyName )
8795 if err != nil {
8896 return nil , false , err
8997 }
@@ -117,7 +125,7 @@ func (cs *CSCloud) EnsureLoadBalancer(ctx context.Context, clusterName string, s
117125 // Get the load balancer details and existing rules.
118126 name := cs .GetLoadBalancerName (ctx , clusterName , service )
119127 legacyName := cs .getLoadBalancerLegacyName (ctx , clusterName , service )
120- lb , err := cs .getLoadBalancerByName ( name , legacyName )
128+ lb , err := cs .getLoadBalancer ( service , name , legacyName )
121129 if err != nil {
122130 return nil , err
123131 }
@@ -175,8 +183,10 @@ func (cs *CSCloud) EnsureLoadBalancer(ctx context.Context, clusterName string, s
175183
176184 klog .V (4 ).Infof ("Load balancer %v is associated with IP %v" , lb .name , lb .ipAddr )
177185
178- // Set the load balancer IP address annotation on the Service
186+ // Set the load balancer annotations on the Service
179187 setServiceAnnotation (service , ServiceAnnotationLoadBalancerAddress , lb .ipAddr )
188+ setServiceAnnotation (service , ServiceAnnotationLoadBalancerID , lb .ipAddrID )
189+ setServiceAnnotation (service , ServiceAnnotationLoadBalancerNetworkID , lb .networkID )
180190
181191 for _ , port := range service .Spec .Ports {
182192 // Construct the protocol name first, we need it a few times
@@ -281,7 +291,7 @@ func (cs *CSCloud) UpdateLoadBalancer(ctx context.Context, clusterName string, s
281291 // Get the load balancer details and existing rules.
282292 name := cs .GetLoadBalancerName (ctx , clusterName , service )
283293 legacyName := cs .getLoadBalancerLegacyName (ctx , clusterName , service )
284- lb , err := cs .getLoadBalancerByName ( name , legacyName )
294+ lb , err := cs .getLoadBalancer ( service , name , legacyName )
285295 if err != nil {
286296 return err
287297 }
@@ -320,7 +330,7 @@ func (cs *CSCloud) EnsureLoadBalancerDeleted(ctx context.Context, clusterName st
320330 // Get the load balancer details and existing rules.
321331 name := cs .GetLoadBalancerName (ctx , clusterName , service )
322332 legacyName := cs .getLoadBalancerLegacyName (ctx , clusterName , service )
323- lb , err := cs .getLoadBalancerByName ( name , legacyName )
333+ lb , err := cs .getLoadBalancer ( service , name , legacyName )
324334 if err != nil {
325335 return err
326336 }
@@ -528,6 +538,28 @@ func filterRulesByPrefix(rules []*cloudstack.LoadBalancerRule, prefix string) []
528538 return filtered
529539}
530540
541+ // getLoadBalancer tries to find the load balancer using ID-based lookup first (if annotations
542+ // are present), then falls back to the keyword-based name lookup.
543+ func (cs * CSCloud ) getLoadBalancer (service * corev1.Service , name , legacyName string ) (* loadBalancer , error ) {
544+ if ipAddrID := getLoadBalancerID (service ); ipAddrID != "" {
545+ networkID := getLoadBalancerNetworkID (service )
546+ klog .V (4 ).Infof ("Attempting ID-based load balancer lookup: ipAddrID=%v, networkID=%v" , ipAddrID , networkID )
547+
548+ lb , err := cs .getLoadBalancerByID (name , ipAddrID , networkID )
549+ if err != nil {
550+ return nil , err
551+ }
552+
553+ if len (lb .rules ) > 0 {
554+ return lb , nil
555+ }
556+
557+ klog .V (4 ).Infof ("ID-based lookup returned no rules, falling back to name-based lookup" )
558+ }
559+
560+ return cs .getLoadBalancerByName (name , legacyName )
561+ }
562+
531563// getLoadBalancerByName retrieves the IP address and ID and all the existing rules it can find.
532564func (cs * CSCloud ) getLoadBalancerByName (name , legacyName string ) (* loadBalancer , error ) {
533565 lb := & loadBalancer {
@@ -588,6 +620,50 @@ func (cs *CSCloud) getLoadBalancerByName(name, legacyName string) (*loadBalancer
588620 return lb , nil
589621}
590622
623+ // getLoadBalancerByID retrieves load balancer rules by public IP ID and network ID.
624+ // This is more reliable than keyword-based search as it uses exact ID matching.
625+ func (cs * CSCloud ) getLoadBalancerByID (name , ipAddrID , networkID string ) (* loadBalancer , error ) {
626+ lb := & loadBalancer {
627+ CloudStackClient : cs .client ,
628+ name : name ,
629+ projectID : cs .projectID ,
630+ rules : make (map [string ]* cloudstack.LoadBalancerRule ),
631+ }
632+
633+ p := cs .client .LoadBalancer .NewListLoadBalancerRulesParams ()
634+ p .SetPublicipid (ipAddrID )
635+ p .SetListall (true )
636+
637+ if networkID != "" {
638+ p .SetNetworkid (networkID )
639+ }
640+
641+ if cs .projectID != "" {
642+ p .SetProjectid (cs .projectID )
643+ }
644+
645+ l , err := cs .client .LoadBalancer .ListLoadBalancerRules (p )
646+ if err != nil {
647+ return nil , fmt .Errorf ("error retrieving load balancer rules by IP ID %v: %w" , ipAddrID , err )
648+ }
649+
650+ for _ , lbRule := range l .LoadBalancerRules {
651+ lb .rules [lbRule .Name ] = lbRule
652+
653+ if lb .ipAddr != "" && lb .ipAddr != lbRule .Publicip {
654+ klog .Warningf ("Load balancer %v has rules associated with different IP's: %v, %v" , lb .name , lb .ipAddr , lbRule .Publicip )
655+ }
656+
657+ lb .ipAddr = lbRule .Publicip
658+ lb .ipAddrID = lbRule .Publicipid
659+ lb .networkID = lbRule .Networkid
660+ }
661+
662+ klog .V (4 ).Infof ("Load balancer %v (by ID %v) contains %d rule(s)" , lb .name , ipAddrID , len (lb .rules ))
663+
664+ return lb , nil
665+ }
666+
591667// verifyHosts verifies if all hosts belong to the same network, and returns the host ID's and network ID.
592668// During rolling upgrades some nodes may not yet have a corresponding VM in CloudStack, so we tolerate
593669// partial matches: as long as at least one node can be resolved we return the matched set and log
@@ -1328,6 +1404,16 @@ func getLoadBalancerAddress(service *corev1.Service) string {
13281404 return service .Spec .LoadBalancerIP //nolint:staticcheck // deprecated but kept as fallback
13291405}
13301406
1407+ // getLoadBalancerID returns the stored load balancer public IP UUID from the service annotation.
1408+ func getLoadBalancerID (service * corev1.Service ) string {
1409+ return getStringFromServiceAnnotation (service , ServiceAnnotationLoadBalancerID , "" )
1410+ }
1411+
1412+ // getLoadBalancerNetworkID returns the stored load balancer network UUID from the service annotation.
1413+ func getLoadBalancerNetworkID (service * corev1.Service ) string {
1414+ return getStringFromServiceAnnotation (service , ServiceAnnotationLoadBalancerNetworkID , "" )
1415+ }
1416+
13311417// setServiceAnnotation is used to create/set or update an annotation on the Service object.
13321418func setServiceAnnotation (service * corev1.Service , key , value string ) {
13331419 if service .ObjectMeta .Annotations == nil {
0 commit comments