Skip to content

Commit 3a8a2c1

Browse files
committed
Add support to update the loadbalancer rule when source cidr list is updated
1 parent 4d8bb2e commit 3a8a2c1

File tree

3 files changed

+125
-28
lines changed

3 files changed

+125
-28
lines changed

cloudstack.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ import (
2525
"fmt"
2626
"io"
2727
"os"
28+
"strings"
2829

2930
"github.com/apache/cloudstack-go/v2/cloudstack"
31+
"github.com/blang/semver/v4"
3032
"gopkg.in/gcfg.v1"
3133
"k8s.io/apimachinery/pkg/types"
3234
cloudprovider "k8s.io/cloud-provider"
@@ -53,6 +55,7 @@ type CSCloud struct {
5355
client *cloudstack.CloudStackClient
5456
projectID string // If non-"", all resources will be created within this project
5557
zone string
58+
version semver.Version
5659
}
5760

5861
func init() {
@@ -85,6 +88,7 @@ func newCSCloud(cfg *CSConfig) (*CSCloud, error) {
8588
cs := &CSCloud{
8689
projectID: cfg.Global.ProjectID,
8790
zone: cfg.Global.Zone,
91+
version: semver.Version{},
8892
}
8993

9094
if cfg.Global.APIURL != "" && cfg.Global.APIKey != "" && cfg.Global.SecretKey != "" {
@@ -95,9 +99,32 @@ func newCSCloud(cfg *CSConfig) (*CSCloud, error) {
9599
return nil, errors.New("no cloud provider config given")
96100
}
97101

102+
version, err := cs.getManagementServerVersion()
103+
if err != nil {
104+
return nil, err
105+
}
106+
cs.version = version
107+
98108
return cs, nil
99109
}
100110

111+
func (cs *CSCloud) getManagementServerVersion() (semver.Version, error) {
112+
msServersResp, err := cs.client.Management.ListManagementServersMetrics(cs.client.Management.NewListManagementServersMetricsParams())
113+
if err != nil {
114+
return semver.Version{}, err
115+
}
116+
if msServersResp.Count == 0 {
117+
return semver.Version{}, errors.New("no management servers found")
118+
}
119+
version := msServersResp.ManagementServersMetrics[0].Version
120+
v, err := semver.ParseTolerant(strings.Join(strings.Split(version, ".")[0:3], "."))
121+
if err != nil {
122+
klog.Errorf("failed to parse management server version: %v", err)
123+
return semver.Version{}, err
124+
}
125+
return v, nil
126+
}
127+
101128
// Initialize passes a Kubernetes clientBuilder interface to the cloud provider
102129
func (cs *CSCloud) Initialize(clientBuilder cloudprovider.ControllerClientBuilder, stop <-chan struct{}) {
103130
}

cloudstack_loadbalancer.go

Lines changed: 97 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"strings"
2828

2929
"github.com/apache/cloudstack-go/v2/cloudstack"
30+
"github.com/blang/semver/v4"
3031
"k8s.io/klog/v2"
3132

3233
corev1 "k8s.io/api/core/v1"
@@ -44,7 +45,12 @@ const (
4445
// CloudStack >= 4.6 is required for it to work.
4546
ServiceAnnotationLoadBalancerProxyProtocol = "service.beta.kubernetes.io/cloudstack-load-balancer-proxy-protocol"
4647
ServiceAnnotationLoadBalancerLoadbalancerHostname = "service.beta.kubernetes.io/cloudstack-load-balancer-hostname"
47-
ServiceAnnotationLoadBalancerSourceCidrs = "service.beta.kubernetes.io/cloudstack-load-balancer-source-cidrs"
48+
49+
// ServiceAnnotationLoadBalancerSourceCidrs is the annotation used on the
50+
// service to specify the source CIDR list for a CloudStack load balancer.
51+
// The CIDR list is a comma-separated list of CIDR ranges (e.g., "10.0.0.0/8,192.168.1.0/24").
52+
// If not specified, the default is to allow all sources ("0.0.0.0/0").
53+
ServiceAnnotationLoadBalancerSourceCidrs = "service.beta.kubernetes.io/cloudstack-load-balancer-source-cidrs"
4854
)
4955

5056
type loadBalancer struct {
@@ -143,15 +149,15 @@ func (cs *CSCloud) EnsureLoadBalancer(ctx context.Context, clusterName string, s
143149
lbRuleName := fmt.Sprintf("%s-%s-%d", lb.name, protocol, port.Port)
144150

145151
// If the load balancer rule exists and is up-to-date, we move on to the next rule.
146-
lbRule, needsUpdate, err := lb.checkLoadBalancerRule(lbRuleName, port, protocol)
152+
lbRule, needsUpdate, err := lb.checkLoadBalancerRule(lbRuleName, port, protocol, service, cs.version)
147153
if err != nil {
148154
return nil, err
149155
}
150156

151157
if lbRule != nil {
152158
if needsUpdate {
153159
klog.V(4).Infof("Updating load balancer rule: %v", lbRuleName)
154-
if err := lb.updateLoadBalancerRule(lbRuleName, protocol); err != nil {
160+
if err := lb.updateLoadBalancerRule(lbRuleName, protocol, service, cs.version); err != nil {
155161
return nil, err
156162
}
157163
// Delete the rule from the map, to prevent it being deleted.
@@ -561,37 +567,111 @@ func (lb *loadBalancer) releaseLoadBalancerIP() error {
561567
return nil
562568
}
563569

570+
func (lb *loadBalancer) getCIDRList(service *corev1.Service) ([]string, error) {
571+
sourceCIDRs := getStringFromServiceAnnotation(service, ServiceAnnotationLoadBalancerSourceCidrs, defaultAllowedCIDR)
572+
var cidrList []string
573+
if sourceCIDRs != "" {
574+
cidrList = strings.Split(sourceCIDRs, ",")
575+
for i, cidr := range cidrList {
576+
cidr = strings.TrimSpace(cidr)
577+
if _, _, err := net.ParseCIDR(cidr); err != nil {
578+
return nil, fmt.Errorf("invalid CIDR %s in annotation %s: %w", cidr, ServiceAnnotationLoadBalancerSourceCidrs, err)
579+
}
580+
cidrList[i] = cidr
581+
}
582+
}
583+
return cidrList, nil
584+
}
585+
564586
// checkLoadBalancerRule checks if the rule already exists and if it does, if it can be updated. If
565587
// it does exist but cannot be updated, it will delete the existing rule so it can be created again.
566-
func (lb *loadBalancer) checkLoadBalancerRule(lbRuleName string, port corev1.ServicePort, protocol LoadBalancerProtocol) (*cloudstack.LoadBalancerRule, bool, error) {
588+
func (lb *loadBalancer) checkLoadBalancerRule(lbRuleName string, port corev1.ServicePort, protocol LoadBalancerProtocol, service *corev1.Service, version semver.Version) (*cloudstack.LoadBalancerRule, bool, error) {
567589
lbRule, ok := lb.rules[lbRuleName]
568590
if !ok {
569591
return nil, false, nil
570592
}
571593

572-
// Check if any of the values we cannot update (those that require a new load balancer rule) are changed.
573-
if lbRule.Publicip == lb.ipAddr && lbRule.Privateport == strconv.Itoa(int(port.NodePort)) && lbRule.Publicport == strconv.Itoa(int(port.Port)) {
574-
updateAlgo := lbRule.Algorithm != lb.algorithm
575-
updateProto := lbRule.Protocol != protocol.CSProtocol()
576-
return lbRule, updateAlgo || updateProto, nil
594+
cidrList, err := lb.getCIDRList(service)
595+
if err != nil {
596+
return nil, false, err
577597
}
578598

579-
// Delete the load balancer rule so we can create a new one using the new values.
580-
if err := lb.deleteLoadBalancerRule(lbRule); err != nil {
581-
return nil, false, err
599+
var lbRuleCidrList []string
600+
if lbRule.Cidrlist != "" {
601+
lbRuleCidrList = strings.Split(lbRule.Cidrlist, " ")
602+
for i, cidr := range lbRuleCidrList {
603+
cidr = strings.TrimSpace(cidr)
604+
lbRuleCidrList[i] = cidr
605+
}
606+
}
607+
608+
// Check if basic properties match (IP and ports). If not, we need to recreate the rule.
609+
basicPropsMatch := lbRule.Publicip == lb.ipAddr &&
610+
lbRule.Privateport == strconv.Itoa(int(port.NodePort)) &&
611+
lbRule.Publicport == strconv.Itoa(int(port.Port))
612+
613+
cidrListChanged := len(cidrList) != len(lbRuleCidrList) || !setsEqual(cidrList, lbRuleCidrList)
614+
615+
// Check if CIDR list also changed and version < 4.22, then we must recreate the rule.
616+
if !basicPropsMatch || (cidrListChanged && version.LT(semver.Version{Major: 4, Minor: 22, Patch: 0})) {
617+
// Delete the load balancer rule so we can create a new one using the new values.
618+
if err := lb.deleteLoadBalancerRule(lbRule); err != nil {
619+
return nil, false, err
620+
}
621+
return nil, false, nil
622+
}
623+
624+
// Rule can be updated. Check what needs updating.
625+
updateAlgo := lbRule.Algorithm != lb.algorithm
626+
updateProto := lbRule.Protocol != protocol.CSProtocol()
627+
updateCidrlist := cidrListChanged
628+
629+
return lbRule, updateAlgo || updateProto || updateCidrlist, nil
630+
}
631+
632+
// setsEqual checks if two slices contain the exact same unique elements, regardless of order.
633+
func setsEqual(listA, listB []string) bool {
634+
createSet := func(list []string) map[string]bool {
635+
set := make(map[string]bool)
636+
for _, item := range list {
637+
set[item] = true
638+
}
639+
return set
640+
}
641+
642+
setA := createSet(listA)
643+
setB := createSet(listB)
644+
645+
if len(setA) != len(setB) {
646+
return false
647+
}
648+
649+
for item := range setA {
650+
if _, found := setB[item]; !found {
651+
return false
652+
}
582653
}
583654

584-
return nil, false, nil
655+
return true
585656
}
586657

587658
// updateLoadBalancerRule updates a load balancer rule.
588-
func (lb *loadBalancer) updateLoadBalancerRule(lbRuleName string, protocol LoadBalancerProtocol) error {
659+
func (lb *loadBalancer) updateLoadBalancerRule(lbRuleName string, protocol LoadBalancerProtocol, service *corev1.Service, version semver.Version) error {
589660
lbRule := lb.rules[lbRuleName]
590661

591662
p := lb.LoadBalancer.NewUpdateLoadBalancerRuleParams(lbRule.Id)
592663
p.SetAlgorithm(lb.algorithm)
593664
p.SetProtocol(protocol.CSProtocol())
594665

666+
// TODO: Uncomment after updating cloudstack-go SDK to support CIDR list update.
667+
// if version.GTE(semver.Version{Major: 4, Minor: 22, Patch: 0}) {
668+
// cidrList, err := lb.getCIDRList(service)
669+
// if err != nil {
670+
// return err
671+
// }
672+
// p.SetCidrlist(strings.Join(cidrList, ","))
673+
// }
674+
595675
_, err := lb.LoadBalancer.UpdateLoadBalancerRule(p)
596676
return err
597677
}
@@ -613,19 +693,9 @@ func (lb *loadBalancer) createLoadBalancerRule(lbRuleName string, port corev1.Se
613693
p.SetOpenfirewall(false)
614694

615695
// Read the source CIDR annotation
616-
sourceCIDRs, ok := service.Annotations[ServiceAnnotationLoadBalancerSourceCidrs]
617-
var cidrList []string
618-
if ok && sourceCIDRs != "" {
619-
cidrList = strings.Split(sourceCIDRs, ",")
620-
for i, cidr := range cidrList {
621-
cidr = strings.TrimSpace(cidr)
622-
if _, _, err := net.ParseCIDR(cidr); err != nil {
623-
return nil, fmt.Errorf("invalid CIDR in annotation %s: %s", ServiceAnnotationLoadBalancerSourceCidrs, cidr)
624-
}
625-
cidrList[i] = cidr
626-
}
627-
} else {
628-
cidrList = []string{defaultAllowedCIDR}
696+
cidrList, err := lb.getCIDRList(service)
697+
if err != nil {
698+
return nil, err
629699
}
630700

631701
// Set the CIDR list in the parameters

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ go 1.23.0
44

55
require (
66
github.com/apache/cloudstack-go/v2 v2.17.1
7+
github.com/blang/semver/v4 v4.0.0
78
github.com/spf13/pflag v1.0.5
89
gopkg.in/gcfg.v1 v1.2.3
910
k8s.io/api v0.24.17
@@ -17,7 +18,6 @@ require (
1718
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
1819
github.com/NYTimes/gziphandler v1.1.1 // indirect
1920
github.com/beorn7/perks v1.0.1 // indirect
20-
github.com/blang/semver/v4 v4.0.0 // indirect
2121
github.com/cespare/xxhash/v2 v2.2.0 // indirect
2222
github.com/coreos/go-semver v0.3.0 // indirect
2323
github.com/coreos/go-systemd/v22 v22.3.2 // indirect

0 commit comments

Comments
 (0)