@@ -45,19 +45,25 @@ const (
4545 ServiceAnnotationLoadBalancerProxyProtocol = "service.beta.kubernetes.io/cloudstack-load-balancer-proxy-protocol"
4646 ServiceAnnotationLoadBalancerLoadbalancerHostname = "service.beta.kubernetes.io/cloudstack-load-balancer-hostname"
4747 ServiceAnnotationLoadBalancerSourceCidrs = "service.beta.kubernetes.io/cloudstack-load-balancer-source-cidrs"
48+
49+ // ServiceAnnotationLoadBalancerIPAssociatedByController indicates that the controller
50+ // associated the IP address. This annotation is set by the controller when it associates
51+ // an unallocated IP, and is used to determine if the IP should be disassociated on deletion.
52+ ServiceAnnotationLoadBalancerIPAssociatedByController = "service.beta.kubernetes.io/cloudstack-load-balancer-ip-associated-by-controller" //nolint:gosec
4853)
4954
5055type loadBalancer struct {
5156 * cloudstack.CloudStackClient
5257
53- name string
54- algorithm string
55- hostIDs []string
56- ipAddr string
57- ipAddrID string
58- networkID string
59- projectID string
60- rules map [string ]* cloudstack.LoadBalancerRule
58+ name string
59+ algorithm string
60+ hostIDs []string
61+ ipAddr string
62+ ipAddrID string
63+ networkID string
64+ projectID string
65+ rules map [string ]* cloudstack.LoadBalancerRule
66+ ipAssociatedByController bool
6167}
6268
6369// GetLoadBalancer returns whether the specified load balancer exists, and if so, what its status is.
@@ -128,6 +134,14 @@ func (cs *CSCloud) EnsureLoadBalancer(ctx context.Context, clusterName string, s
128134 }
129135 }(lb )
130136 }
137+
138+ // If the controller associated the IP and matches the service spec, set the annotation to persist this information.
139+ if lb .ipAssociatedByController && lb .ipAddr == service .Spec .LoadBalancerIP {
140+ if err := cs .setServiceAnnotation (ctx , service , ServiceAnnotationLoadBalancerIPAssociatedByController , "true" ); err != nil {
141+ // Log the error but don't fail - the annotation is helpful but not critical
142+ klog .Warningf ("Failed to set annotation on service %s/%s: %v" , service .Namespace , service .Name , err )
143+ }
144+ }
131145 }
132146
133147 klog .V (4 ).Infof ("Load balancer %v is associated with IP %v" , lb .name , lb .ipAddr )
@@ -201,11 +215,11 @@ func (cs *CSCloud) EnsureLoadBalancer(ctx context.Context, clusterName string, s
201215 for _ , lbRule := range lb .rules {
202216 protocol := ProtocolFromLoadBalancer (lbRule .Protocol )
203217 if protocol == LoadBalancerProtocolInvalid {
204- return nil , fmt .Errorf ("Error parsing protocol %v: %v" , lbRule .Protocol , err )
218+ return nil , fmt .Errorf ("error parsing protocol %v: %v" , lbRule .Protocol , err )
205219 }
206220 port , err := strconv .ParseInt (lbRule .Publicport , 10 , 32 )
207221 if err != nil {
208- return nil , fmt .Errorf ("Error parsing port %s: %v" , lbRule .Publicport , err )
222+ return nil , fmt .Errorf ("error parsing port %s: %v" , lbRule .Publicport , err )
209223 }
210224
211225 klog .V (4 ).Infof ("Deleting firewall rules associated with load balancer rule: %v (%v:%v:%v)" , lbRule .Name , protocol , lbRule .Publicip , port )
@@ -354,10 +368,52 @@ func (cs *CSCloud) EnsureLoadBalancerDeleted(ctx context.Context, clusterName st
354368 }
355369 }
356370
357- if lb .ipAddr != "" && lb .ipAddr != service .Spec .LoadBalancerIP {
358- klog .V (4 ).Infof ("Releasing load balancer IP: %v" , lb .ipAddr )
359- if err := lb .releaseLoadBalancerIP (); err != nil {
360- return err
371+ if lb .ipAddr != "" {
372+ // If the IP was allocated by the controller (not specified in service spec), release it.
373+ if lb .ipAddr != service .Spec .LoadBalancerIP {
374+ klog .V (4 ).Infof ("Releasing load balancer IP: %v" , lb .ipAddr )
375+ if err := lb .releaseLoadBalancerIP (); err != nil {
376+ return err
377+ }
378+ } else {
379+ // If the IP was specified in service spec, check if it was associated by the controller.
380+ // First, check if there's an annotation indicating the controller associated it.
381+ // If not, check if there are any other load balancer rules using this IP.
382+ shouldDisassociate := getBoolFromServiceAnnotation (service , ServiceAnnotationLoadBalancerIPAssociatedByController , false )
383+
384+ if shouldDisassociate {
385+ // Annotation is set, so check if there are any other load balancer rules using this IP.
386+ // Since we've already deleted all rules for this service, any remaining rules must belong
387+ // to other services. If no other rules exist, it's safe to disassociate the IP.
388+ ip , count , err := lb .Address .GetPublicIpAddressByID (lb .ipAddrID )
389+ if err != nil {
390+ klog .Errorf ("Error retrieving IP address %v for disassociation check: %v" , lb .ipAddr , err )
391+ shouldDisassociate = false
392+ } else if count > 0 && ip .Allocated != "" {
393+ p := lb .LoadBalancer .NewListLoadBalancerRulesParams ()
394+ p .SetPublicipid (lb .ipAddrID )
395+ p .SetListall (true )
396+ if lb .projectID != "" {
397+ p .SetProjectid (lb .projectID )
398+ }
399+ otherRules , err := lb .LoadBalancer .ListLoadBalancerRules (p )
400+ if err != nil {
401+ klog .Errorf ("Error checking for other load balancer rules using IP %v: %v" , lb .ipAddr , err )
402+ shouldDisassociate = false
403+ } else if otherRules .Count > 0 {
404+ // Other load balancer rules are using this IP (other services are using it),
405+ // so don't disassociate.
406+ shouldDisassociate = false
407+ }
408+ }
409+ }
410+
411+ if shouldDisassociate {
412+ klog .V (4 ).Infof ("Disassociating IP %v that was associated by the controller" , lb .ipAddr )
413+ if err := lb .releaseLoadBalancerIP (); err != nil {
414+ return err
415+ }
416+ }
361417 }
362418 }
363419
@@ -492,6 +548,7 @@ func (lb *loadBalancer) getPublicIPAddress(loadBalancerIP string) error {
492548
493549 p := lb .Address .NewListPublicIpAddressesParams ()
494550 p .SetIpaddress (loadBalancerIP )
551+ p .SetAllocatedonly (false )
495552 p .SetListall (true )
496553
497554 if lb .projectID != "" {
@@ -504,12 +561,16 @@ func (lb *loadBalancer) getPublicIPAddress(loadBalancerIP string) error {
504561 }
505562
506563 if l .Count != 1 {
507- return fmt .Errorf ("could not find IP address %v" , loadBalancerIP )
564+ return fmt .Errorf ("could not find IP address %v. Found %d addresses " , loadBalancerIP , l . Count )
508565 }
509566
510567 lb .ipAddr = l .PublicIpAddresses [0 ].Ipaddress
511568 lb .ipAddrID = l .PublicIpAddresses [0 ].Id
512569
570+ // If the IP is not allocated, associate it.
571+ if l .PublicIpAddresses [0 ].Allocated == "" {
572+ return lb .associatePublicIPAddress ()
573+ }
513574 return nil
514575}
515576
@@ -538,6 +599,10 @@ func (lb *loadBalancer) associatePublicIPAddress() error {
538599 p .SetProjectid (lb .projectID )
539600 }
540601
602+ if lb .ipAddr != "" {
603+ p .SetIpaddress (lb .ipAddr )
604+ }
605+
541606 // Associate a new IP address
542607 r , err := lb .Address .AssociateIpAddress (p )
543608 if err != nil {
@@ -546,6 +611,7 @@ func (lb *loadBalancer) associatePublicIPAddress() error {
546611
547612 lb .ipAddr = r .Ipaddress
548613 lb .ipAddrID = r .Id
614+ lb .ipAssociatedByController = true
549615
550616 return nil
551617}
0 commit comments