@@ -96,7 +96,7 @@ func (cs *CSCloud) GetLoadBalancer(ctx context.Context, clusterName string, serv
9696}
9797
9898// EnsureLoadBalancer creates a new load balancer, or updates the existing one. Returns the status of the balancer.
99- func (cs * CSCloud ) EnsureLoadBalancer (ctx context.Context , clusterName string , service * corev1.Service , nodes []* corev1.Node ) (status * corev1.LoadBalancerStatus , err error ) { //nolint:gocognit,gocyclo,nestif
99+ func (cs * CSCloud ) EnsureLoadBalancer (ctx context.Context , clusterName string , service * corev1.Service , nodes []* corev1.Node ) (status * corev1.LoadBalancerStatus , err error ) { //nolint:gocognit,gocyclo,nestif,maintidx
100100 klog .V (4 ).InfoS ("EnsureLoadBalancer" , "cluster" , clusterName , "service" , klog .KObj (service ))
101101 serviceName := fmt .Sprintf ("%s/%s" , service .Namespace , service .Name )
102102
@@ -141,7 +141,6 @@ func (cs *CSCloud) EnsureLoadBalancer(ctx context.Context, clusterName string, s
141141 msg := fmt .Sprintf ("Created new load balancer for service %s with algorithm '%s' and IP address %s" , serviceName , lb .algorithm , lb .ipAddr )
142142 cs .eventRecorder .Event (service , corev1 .EventTypeNormal , "CreatedLoadBalancer" , msg )
143143 klog .Info (msg )
144-
145144 } else if service .Spec .LoadBalancerIP != "" && service .Spec .LoadBalancerIP != lb .ipAddr {
146145 // LoadBalancerIP was specified and it's different from the current IP
147146 // Release the old IP first
@@ -161,8 +160,10 @@ func (cs *CSCloud) EnsureLoadBalancer(ctx context.Context, clusterName string, s
161160 // CloudStack sometimes reports deletes as "invalid value" when the entity is already gone.
162161 if strings .Contains (delErr .Error (), "does not exist" ) || strings .Contains (delErr .Error (), "Invalid parameter id value" ) {
163162 klog .V (4 ).Infof ("Load balancer rule %s already removed, continuing: %v" , oldRule .Name , delErr )
163+
164164 continue
165165 }
166+
166167 return nil , delErr
167168 }
168169 }
@@ -172,11 +173,13 @@ func (cs *CSCloud) EnsureLoadBalancer(ctx context.Context, clusterName string, s
172173
173174 if err := lb .releaseLoadBalancerIP (); err != nil {
174175 klog .Errorf ("attempt to release old load balancer IP failed: %s" , err .Error ())
176+
175177 return nil , fmt .Errorf ("failed to release old load balancer IP: %w" , err )
176178 }
177179
178180 if err := lb .getLoadBalancerIP (service .Spec .LoadBalancerIP ); err != nil {
179181 klog .Errorf ("failed to allocate specified IP %v: %v" , service .Spec .LoadBalancerIP , err )
182+
180183 return nil , fmt .Errorf ("failed to allocate specified load balancer IP: %w" , err )
181184 }
182185
@@ -211,13 +214,16 @@ func (cs *CSCloud) EnsureLoadBalancer(ctx context.Context, clusterName string, s
211214 if err := lb .updateLoadBalancerRule (lbRuleName , protocol ); err != nil {
212215 return nil , err
213216 }
214- // Delete the rule from the map, to prevent it being deleted.
215- delete (lb .rules , lbRuleName )
216217 } else {
217218 klog .V (4 ).Infof ("Load balancer rule %v is up-to-date" , lbRuleName )
218- // Delete the rule from the map, to prevent it being deleted.
219- delete (lb .rules , lbRuleName )
220219 }
220+
221+ if err := lb .reconcileHostsForRule (lbRule , lb .hostIDs ); err != nil {
222+ return nil , err
223+ }
224+
225+ // Delete the rule from the map, to prevent it being deleted.
226+ delete (lb .rules , lbRuleName )
221227 } else {
222228 klog .V (4 ).Infof ("Creating load balancer rule: %v" , lbRuleName )
223229 lbRule , err = lb .createLoadBalancerRule (lbRuleName , port , protocol )
@@ -301,34 +307,8 @@ func (cs *CSCloud) UpdateLoadBalancer(ctx context.Context, clusterName string, s
301307 }
302308
303309 for _ , lbRule := range lb .rules {
304- p := lb .LoadBalancer .NewListLoadBalancerRuleInstancesParams (lbRule .Id )
305-
306- // Retrieve all VMs currently associated to this load balancer rule.
307- l , err := lb .LoadBalancer .ListLoadBalancerRuleInstances (p )
308- if err != nil {
309- return fmt .Errorf ("error retrieving associated instances: %w" , err )
310- }
311-
312- assign , remove := symmetricDifference (lb .hostIDs , l .LoadBalancerRuleInstances )
313-
314- klog .V (4 ).Infof ("Load balancer rule %v: %d host(s) to assign, %d host(s) to remove (wanted: %v, current: %d instances)" ,
315- lbRule .Name , len (assign ), len (remove ), lb .hostIDs , len (l .LoadBalancerRuleInstances ))
316-
317- // IMPORTANT: Assign new hosts BEFORE removing old ones to ensure the load balancer
318- // always has backends during rolling upgrades. If assignment fails, we abort without
319- // removing old hosts so traffic can still be served.
320- if len (assign ) > 0 {
321- klog .V (4 ).Infof ("Assigning new hosts (%v) to load balancer rule: %v" , assign , lbRule .Name )
322- if err := lb .assignHostsToRule (lbRule , assign ); err != nil {
323- return fmt .Errorf ("error assigning new hosts to rule %v (old hosts preserved): %w" , lbRule .Name , err )
324- }
325- }
326-
327- if len (remove ) > 0 {
328- klog .V (4 ).Infof ("Removing old hosts (%v) from load balancer rule: %v" , remove , lbRule .Name )
329- if err := lb .removeHostsFromRule (lbRule , remove ); err != nil {
330- return err
331- }
310+ if err := lb .reconcileHostsForRule (lbRule , lb .hostIDs ); err != nil {
311+ return err
332312 }
333313 }
334314
@@ -362,6 +342,7 @@ func (cs *CSCloud) EnsureLoadBalancerDeleted(ctx context.Context, clusterName st
362342 // If no rules exist, the load balancer doesn't exist - nothing to delete
363343 if len (lb .rules ) == 0 {
364344 klog .V (4 ).Infof ("No load balancer rules found for service, nothing to delete" )
345+
365346 return nil
366347 }
367348
@@ -413,16 +394,17 @@ func (cs *CSCloud) EnsureLoadBalancerDeleted(ctx context.Context, clusterName st
413394 }
414395
415396 // Delete the public IP address if appropriate
416- if lb .ipAddr != "" {
397+ if lb .ipAddr != "" { //nolint:nestif
417398 klog .V (4 ).Infof ("Processing public IP deletion for load balancer: IP=%v, ID=%v" , lb .ipAddr , lb .ipAddrID )
418399
419400 // Check if we should release the IP
420401 shouldReleaseIP , err := cs .shouldReleaseLoadBalancerIP (lb , service )
421- if err != nil {
402+ switch {
403+ case err != nil :
422404 err := fmt .Errorf ("error determining if IP should be released: %w" , err )
423405 klog .Errorf ("%v" , err )
424406 deletionErrors = append (deletionErrors , err )
425- } else if shouldReleaseIP {
407+ case shouldReleaseIP :
426408 klog .V (4 ).Infof ("Releasing load balancer IP: %v" , lb .ipAddr )
427409 if err := lb .releaseLoadBalancerIP (); err != nil {
428410 err := fmt .Errorf ("error releasing load balancer IP %v: %w" , lb .ipAddr , err )
@@ -433,7 +415,7 @@ func (cs *CSCloud) EnsureLoadBalancerDeleted(ctx context.Context, clusterName st
433415 cs .eventRecorder .Event (service , corev1 .EventTypeNormal , "ReleasedLoadBalancerIP" , msg )
434416 klog .Info (msg )
435417 }
436- } else {
418+ default :
437419 klog .V (4 ).Infof ("Load balancer IP %v is in use by other services, keeping it allocated" , lb .ipAddr )
438420 }
439421 }
@@ -443,23 +425,25 @@ func (cs *CSCloud) EnsureLoadBalancerDeleted(ctx context.Context, clusterName st
443425 msg := fmt .Sprintf ("Encountered %d error(s) while deleting load balancer for service %s" , len (deletionErrors ), serviceName )
444426 klog .Warningf ("%s: %v" , msg , deletionErrors )
445427 cs .eventRecorder .Event (service , corev1 .EventTypeWarning , "DeletingLoadBalancerFailed" , msg )
428+
446429 // Return the first error or a combined error message
447- return fmt .Errorf ("load balancer deletion completed with errors: %v " , deletionErrors [0 ])
430+ return fmt .Errorf ("load balancer deletion completed with errors: %w " , deletionErrors [0 ])
448431 }
449432
450- msg := fmt . Sprintf ( "Successfully deleted load balancer for service %s" , serviceName )
433+ msg := "Successfully deleted load balancer for service " + serviceName
451434 cs .eventRecorder .Event (service , corev1 .EventTypeNormal , "DeletedLoadBalancer" , msg )
452435 klog .Info (msg )
453436
454437 return nil
455438}
456439
457- // shouldReleaseLoadBalancerIP determines whether the public IP should be released
440+ // shouldReleaseLoadBalancerIP determines whether the public IP should be released.
458441func (cs * CSCloud ) shouldReleaseLoadBalancerIP (lb * loadBalancer , service * corev1.Service ) (bool , error ) {
459442 // If the IP was explicitly specified in the service spec, don't release it
460443 // The user is responsible for managing the lifecycle of user-provided IPs
461444 if service .Spec .LoadBalancerIP != "" && service .Spec .LoadBalancerIP == lb .ipAddr {
462445 klog .V (4 ).Infof ("IP %v was explicitly specified in service spec, not releasing" , lb .ipAddr )
446+
463447 return false , nil
464448 }
465449
@@ -479,11 +463,13 @@ func (cs *CSCloud) shouldReleaseLoadBalancerIP(lb *loadBalancer, service *corev1
479463 // If other rules exist, this IP is in use by other services
480464 if otherRules .Count > 0 {
481465 klog .V (4 ).Infof ("IP %v has %d other load balancer rule(s) in use, not releasing" , lb .ipAddr , otherRules .Count )
466+
482467 return false , nil
483468 }
484469
485470 // IP is safe to release - it's either controller-allocated or no longer in use
486471 klog .V (4 ).Infof ("IP %v is no longer in use and safe to release" , lb .ipAddr )
472+
487473 return true , nil
488474}
489475
@@ -855,6 +841,39 @@ func (lb *loadBalancer) deleteLoadBalancerRule(lbRule *cloudstack.LoadBalancerRu
855841 return nil
856842}
857843
844+ // reconcileHostsForRule ensures the load balancer rule has exactly the expected set of hosts.
845+ // It lists the current members, computes the difference, and assigns new hosts before removing
846+ // old ones so the rule always has backends during rolling upgrades.
847+ func (lb * loadBalancer ) reconcileHostsForRule (lbRule * cloudstack.LoadBalancerRule , hostIDs []string ) error {
848+ p := lb .LoadBalancer .NewListLoadBalancerRuleInstancesParams (lbRule .Id )
849+
850+ l , err := lb .LoadBalancer .ListLoadBalancerRuleInstances (p )
851+ if err != nil {
852+ return fmt .Errorf ("error retrieving associated instances: %w" , err )
853+ }
854+
855+ assign , remove := symmetricDifference (hostIDs , l .LoadBalancerRuleInstances )
856+
857+ klog .V (4 ).Infof ("Reconcile hosts for rule %v: %d host(s) to assign, %d host(s) to remove (wanted: %v, current: %d instances)" ,
858+ lbRule .Name , len (assign ), len (remove ), hostIDs , len (l .LoadBalancerRuleInstances ))
859+
860+ if len (assign ) > 0 {
861+ klog .V (4 ).Infof ("Assigning new hosts (%v) to load balancer rule: %v" , assign , lbRule .Name )
862+ if err := lb .assignHostsToRule (lbRule , assign ); err != nil {
863+ return fmt .Errorf ("error assigning new hosts to rule %v (old hosts preserved): %w" , lbRule .Name , err )
864+ }
865+ }
866+
867+ if len (remove ) > 0 {
868+ klog .V (4 ).Infof ("Removing old hosts (%v) from load balancer rule: %v" , remove , lbRule .Name )
869+ if err := lb .removeHostsFromRule (lbRule , remove ); err != nil {
870+ return err
871+ }
872+ }
873+
874+ return nil
875+ }
876+
858877// assignHostsToRule assigns hosts to a load balancer rule.
859878func (lb * loadBalancer ) assignHostsToRule (lbRule * cloudstack.LoadBalancerRule , hostIDs []string ) error {
860879 p := lb .LoadBalancer .NewAssignToLoadBalancerRuleParams (lbRule .Id )
0 commit comments