Skip to content

Commit b080868

Browse files
authored
Merge pull request #110 from leaseweb/fix/ensure_host_assoc
Fix/Ensure host assocation with LB rules in EnsureLoadBalancer
2 parents 33788c0 + fbe2c98 commit b080868

File tree

8 files changed

+327
-72
lines changed

8 files changed

+327
-72
lines changed

.golangci.yml

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
linters-settings:
2+
gci:
3+
sections:
4+
- standard
5+
- default
6+
- prefix(github.com/apache/cloudstack-kubernetes-provider)
7+
gofumpt:
8+
module-path: "github.com/apache/cloudstack-kubernetes-provider"
9+
nolintlint:
10+
# https://github.com/golangci/golangci-lint/issues/3228
11+
allow-unused: true
12+
13+
linters:
14+
disable:
15+
- goheader
16+
- depguard
17+
- wrapcheck
18+
- mnd
19+
- varnamelen
20+
- testpackage
21+
- tagliatelle
22+
- paralleltest
23+
- ireturn
24+
- err113
25+
- gochecknoglobals
26+
- wsl
27+
- exhaustive
28+
- nosprintfhostport
29+
- nonamedreturns
30+
- interfacebloat
31+
- exhaustruct
32+
- lll
33+
- gosec
34+
- gomoddirectives
35+
- godox
36+
- gochecknoinits
37+
- funlen
38+
- dupl
39+
- cyclop
40+
- inamedparam
41+
# deprecated linters
42+
- tenv
43+
enable-all: true
44+
45+
issues:
46+
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
47+
max-issues-per-linter: 0
48+
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
49+
max-same-issues: 0
50+
exclude-rules:
51+
- path: (.+)_test.go$
52+
linters:
53+
- gocognit
54+
- goconst
55+
- gocyclo
56+
- errcheck
57+
- dupl
58+
- gosec
59+
- forbidigo
60+
- maintidx
61+
62+
run:
63+
timeout: 10m
64+
allow-parallel-runners: true
65+

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ BIN_DIR ?= bin
4040
GO_INSTALL := ./hack/go_install.sh
4141

4242
GOLANGCI_LINT_BIN := golangci-lint
43-
GOLANGCI_LINT_VER := v1.63.4
43+
GOLANGCI_LINT_VER := v1.64.8
4444
GOLANGCI_LINT := $(abspath $(TOOLS_BIN_DIR)/$(GOLANGCI_LINT_BIN)-$(GOLANGCI_LINT_VER))
4545
GOLANGCI_LINT_PKG := github.com/golangci/golangci-lint/cmd/golangci-lint
4646

cloudstack/cloudstack_instances_test.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package cloudstack
22

33
import (
4-
"context"
54
"errors"
65
"testing"
76

@@ -94,8 +93,10 @@ func TestInstanceExists(t *testing.T) {
9493
mockCtrl := gomock.NewController(t)
9594
defer mockCtrl.Finish()
9695

96+
ctx := t.Context()
97+
9798
cs := cloudstack.NewMockClient(mockCtrl)
98-
ms := cs.VirtualMachine.(*cloudstack.MockVirtualMachineServiceIface) //nolint: forcetypeassert
99+
ms := cs.VirtualMachine.(*cloudstack.MockVirtualMachineServiceIface) //nolint:forcetypeassert
99100

100101
fakeInstances := &CSCloud{
101102
client: cs,
@@ -142,13 +143,13 @@ func TestInstanceExists(t *testing.T) {
142143
if test.node.Name == "testDummyVM" {
143144
ms.EXPECT().GetVirtualMachineByName("testDummyVM", gomock.Any()).Return(test.mockedCSOutput, 1, nil)
144145
} else {
145-
ms.EXPECT().GetVirtualMachineByName("nonExistingVM", gomock.Any()).Return(test.mockedCSOutput, 0, errors.New("No match found for ...")) //nolint: revive
146+
ms.EXPECT().GetVirtualMachineByName("nonExistingVM", gomock.Any()).Return(test.mockedCSOutput, 0, errors.New("No match found for ...")) //nolint:revive
146147
}
147148
} else {
148149
ms.EXPECT().GetVirtualMachineByID("915653c4-298b-4d74-bdee-4ced282114f1", gomock.Any()).Return(test.mockedCSOutput, 1, nil)
149150
}
150151

151-
exists, err := fakeInstances.InstanceExists(context.TODO(), test.node)
152+
exists, err := fakeInstances.InstanceExists(ctx, test.node)
152153
if err != nil {
153154
t.Errorf("InstanceExists failed with node %v: %v", nodeName, err)
154155
}
@@ -164,8 +165,10 @@ func TestInstanceShutdown(t *testing.T) {
164165
mockCtrl := gomock.NewController(t)
165166
defer mockCtrl.Finish()
166167

168+
ctx := t.Context()
169+
167170
cs := cloudstack.NewMockClient(mockCtrl)
168-
ms := cs.VirtualMachine.(*cloudstack.MockVirtualMachineServiceIface) //nolint: forcetypeassert
171+
ms := cs.VirtualMachine.(*cloudstack.MockVirtualMachineServiceIface) //nolint:forcetypeassert
169172

170173
fakeInstances := &CSCloud{
171174
client: cs,
@@ -225,7 +228,7 @@ func TestInstanceShutdown(t *testing.T) {
225228
ms.EXPECT().GetVirtualMachineByID("915653c4-298b-4d74-bdee-4ced282114f1", gomock.Any()).Return(test.mockedCSOutput, 1, nil)
226229
}
227230

228-
shutdown, err := fakeInstances.InstanceShutdown(context.TODO(), test.node)
231+
shutdown, err := fakeInstances.InstanceShutdown(ctx, test.node)
229232
if err != nil {
230233
t.Logf("InstanceShutdown failed with node %v: %v", nodeName, err)
231234
}
@@ -241,8 +244,10 @@ func TestInstanceMetadata(t *testing.T) {
241244
mockCtrl := gomock.NewController(t)
242245
defer mockCtrl.Finish()
243246

247+
ctx := t.Context()
248+
244249
cs := cloudstack.NewMockClient(mockCtrl)
245-
ms := cs.VirtualMachine.(*cloudstack.MockVirtualMachineServiceIface) //nolint: forcetypeassert
250+
ms := cs.VirtualMachine.(*cloudstack.MockVirtualMachineServiceIface) //nolint:forcetypeassert
246251

247252
fakeInstances := &CSCloud{
248253
client: cs,
@@ -374,7 +379,7 @@ func TestInstanceMetadata(t *testing.T) {
374379
ms.EXPECT().GetVirtualMachineByID("915653c4-298b-4d74-bdee-4ced282114f1", gomock.Any()).Return(test.mockedCSOutput, 1, nil)
375380
}
376381

377-
metadata, err := fakeInstances.InstanceMetadata(context.TODO(), test.node)
382+
metadata, err := fakeInstances.InstanceMetadata(ctx, test.node)
378383
if err != nil {
379384
t.Logf("InstanceMetadata failed with node %v: %v", nodeName, err)
380385
}

cloudstack/cloudstack_loadbalancer.go

Lines changed: 60 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
458441
func (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.
859878
func (lb *loadBalancer) assignHostsToRule(lbRule *cloudstack.LoadBalancerRule, hostIDs []string) error {
860879
p := lb.LoadBalancer.NewAssignToLoadBalancerRuleParams(lbRule.Id)

0 commit comments

Comments
 (0)