@@ -44,19 +44,25 @@ const (
4444 ServiceAnnotationLoadBalancerProxyProtocol = "service.beta.kubernetes.io/cloudstack-load-balancer-proxy-protocol"
4545
4646 ServiceAnnotationLoadBalancerLoadbalancerHostname = "service.beta.kubernetes.io/cloudstack-load-balancer-hostname"
47+
48+ // ServiceAnnotationLoadBalancerIPAssociatedByController indicates that the controller
49+ // associated the IP address. This annotation is set by the controller when it associates
50+ // an unallocated IP, and is used to determine if the IP should be disassociated on deletion.
51+ ServiceAnnotationLoadBalancerIPAssociatedByController = "service.beta.kubernetes.io/cloudstack-load-balancer-ip-associated-by-controller" //nolint:gosec
4752)
4853
4954type loadBalancer struct {
5055 * cloudstack.CloudStackClient
5156
52- name string
53- algorithm string
54- hostIDs []string
55- ipAddr string
56- ipAddrID string
57- networkID string
58- projectID string
59- rules map [string ]* cloudstack.LoadBalancerRule
57+ name string
58+ algorithm string
59+ hostIDs []string
60+ ipAddr string
61+ ipAddrID string
62+ networkID string
63+ projectID string
64+ rules map [string ]* cloudstack.LoadBalancerRule
65+ ipAssociatedByController bool
6066}
6167
6268// GetLoadBalancer returns whether the specified load balancer exists, and if so, what its status is.
@@ -127,6 +133,14 @@ func (cs *CSCloud) EnsureLoadBalancer(ctx context.Context, clusterName string, s
127133 }
128134 }(lb )
129135 }
136+
137+ // If the controller associated the IP and matches the service spec, set the annotation to persist this information.
138+ if lb .ipAssociatedByController && lb .ipAddr == service .Spec .LoadBalancerIP {
139+ if err := cs .setServiceAnnotation (ctx , service , ServiceAnnotationLoadBalancerIPAssociatedByController , "true" ); err != nil {
140+ // Log the error but don't fail - the annotation is helpful but not critical
141+ klog .Warningf ("Failed to set annotation on service %s/%s: %v" , service .Namespace , service .Name , err )
142+ }
143+ }
130144 }
131145
132146 klog .V (4 ).Infof ("Load balancer %v is associated with IP %v" , lb .name , lb .ipAddr )
@@ -200,11 +214,11 @@ func (cs *CSCloud) EnsureLoadBalancer(ctx context.Context, clusterName string, s
200214 for _ , lbRule := range lb .rules {
201215 protocol := ProtocolFromLoadBalancer (lbRule .Protocol )
202216 if protocol == LoadBalancerProtocolInvalid {
203- return nil , fmt .Errorf ("Error parsing protocol %v: %v" , lbRule .Protocol , err )
217+ return nil , fmt .Errorf ("error parsing protocol %v: %v" , lbRule .Protocol , err )
204218 }
205219 port , err := strconv .ParseInt (lbRule .Publicport , 10 , 32 )
206220 if err != nil {
207- return nil , fmt .Errorf ("Error parsing port %s: %v" , lbRule .Publicport , err )
221+ return nil , fmt .Errorf ("error parsing port %s: %v" , lbRule .Publicport , err )
208222 }
209223
210224 klog .V (4 ).Infof ("Deleting firewall rules associated with load balancer rule: %v (%v:%v:%v)" , lbRule .Name , protocol , lbRule .Publicip , port )
@@ -353,10 +367,52 @@ func (cs *CSCloud) EnsureLoadBalancerDeleted(ctx context.Context, clusterName st
353367 }
354368 }
355369
356- if lb .ipAddr != "" && lb .ipAddr != service .Spec .LoadBalancerIP {
357- klog .V (4 ).Infof ("Releasing load balancer IP: %v" , lb .ipAddr )
358- if err := lb .releaseLoadBalancerIP (); err != nil {
359- return err
370+ if lb .ipAddr != "" {
371+ // If the IP was allocated by the controller (not specified in service spec), release it.
372+ if lb .ipAddr != service .Spec .LoadBalancerIP {
373+ klog .V (4 ).Infof ("Releasing load balancer IP: %v" , lb .ipAddr )
374+ if err := lb .releaseLoadBalancerIP (); err != nil {
375+ return err
376+ }
377+ } else {
378+ // If the IP was specified in service spec, check if it was associated by the controller.
379+ // First, check if there's an annotation indicating the controller associated it.
380+ // If not, check if there are any other load balancer rules using this IP.
381+ shouldDisassociate := getBoolFromServiceAnnotation (service , ServiceAnnotationLoadBalancerIPAssociatedByController , false )
382+
383+ if shouldDisassociate {
384+ // Annotation is set, so check if there are any other load balancer rules using this IP.
385+ // Since we've already deleted all rules for this service, any remaining rules must belong
386+ // to other services. If no other rules exist, it's safe to disassociate the IP.
387+ ip , count , err := lb .Address .GetPublicIpAddressByID (lb .ipAddrID )
388+ if err != nil {
389+ klog .Errorf ("Error retrieving IP address %v for disassociation check: %v" , lb .ipAddr , err )
390+ shouldDisassociate = false
391+ } else if count > 0 && ip .Allocated != "" {
392+ p := lb .LoadBalancer .NewListLoadBalancerRulesParams ()
393+ p .SetPublicipid (lb .ipAddrID )
394+ p .SetListall (true )
395+ if lb .projectID != "" {
396+ p .SetProjectid (lb .projectID )
397+ }
398+ otherRules , err := lb .LoadBalancer .ListLoadBalancerRules (p )
399+ if err != nil {
400+ klog .Errorf ("Error checking for other load balancer rules using IP %v: %v" , lb .ipAddr , err )
401+ shouldDisassociate = false
402+ } else if otherRules .Count > 0 {
403+ // Other load balancer rules are using this IP (other services are using it),
404+ // so don't disassociate.
405+ shouldDisassociate = false
406+ }
407+ }
408+ }
409+
410+ if shouldDisassociate {
411+ klog .V (4 ).Infof ("Disassociating IP %v that was associated by the controller" , lb .ipAddr )
412+ if err := lb .releaseLoadBalancerIP (); err != nil {
413+ return err
414+ }
415+ }
360416 }
361417 }
362418
@@ -491,6 +547,7 @@ func (lb *loadBalancer) getPublicIPAddress(loadBalancerIP string) error {
491547
492548 p := lb .Address .NewListPublicIpAddressesParams ()
493549 p .SetIpaddress (loadBalancerIP )
550+ p .SetAllocatedonly (false )
494551 p .SetListall (true )
495552
496553 if lb .projectID != "" {
@@ -503,12 +560,16 @@ func (lb *loadBalancer) getPublicIPAddress(loadBalancerIP string) error {
503560 }
504561
505562 if l .Count != 1 {
506- return fmt .Errorf ("could not find IP address %v" , loadBalancerIP )
563+ return fmt .Errorf ("could not find IP address %v. Found %d addresses " , loadBalancerIP , l . Count )
507564 }
508565
509566 lb .ipAddr = l .PublicIpAddresses [0 ].Ipaddress
510567 lb .ipAddrID = l .PublicIpAddresses [0 ].Id
511568
569+ // If the IP is not allocated, associate it.
570+ if l .PublicIpAddresses [0 ].Allocated == "" {
571+ return lb .associatePublicIPAddress ()
572+ }
512573 return nil
513574}
514575
@@ -537,6 +598,10 @@ func (lb *loadBalancer) associatePublicIPAddress() error {
537598 p .SetProjectid (lb .projectID )
538599 }
539600
601+ if lb .ipAddr != "" {
602+ p .SetIpaddress (lb .ipAddr )
603+ }
604+
540605 // Associate a new IP address
541606 r , err := lb .Address .AssociateIpAddress (p )
542607 if err != nil {
@@ -545,6 +610,7 @@ func (lb *loadBalancer) associatePublicIPAddress() error {
545610
546611 lb .ipAddr = r .Ipaddress
547612 lb .ipAddrID = r .Id
613+ lb .ipAssociatedByController = true
548614
549615 return nil
550616}
0 commit comments