Skip to content

Commit f25a1e7

Browse files
committed
Feat: Add support for stickyness policy
1 parent 4d8bb2e commit f25a1e7

File tree

1 file changed

+160
-12
lines changed

1 file changed

+160
-12
lines changed

cloudstack_loadbalancer.go

Lines changed: 160 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,23 @@ 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+
ServiceAnnotationLoadBalancerStickynessMethodName = "service.beta.kubernetes.io/cloudstack-load-balancer-stickyness-method-name"
50+
ServiceAnnotationLoadBalancerStickynessParam = "service.beta.kubernetes.io/cloudstack-load-balancer-stickyness-method-param"
4851
)
4952

5053
type loadBalancer struct {
5154
*cloudstack.CloudStackClient
5255

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
56+
name string
57+
algorithm string
58+
hostIDs []string
59+
ipAddr string
60+
ipAddrID string
61+
networkID string
62+
projectID string
63+
rules map[string]*cloudstack.LoadBalancerRule
64+
stickynessPolicies map[string]*cloudstack.LBStickinessPolicyStickinesspolicy
6165
}
6266

6367
// GetLoadBalancer returns whether the specified load balancer exists, and if so, what its status is.
@@ -161,12 +165,36 @@ func (cs *CSCloud) EnsureLoadBalancer(ctx context.Context, clusterName string, s
161165
// Delete the rule from the map, to prevent it being deleted.
162166
delete(lb.rules, lbRuleName)
163167
}
168+
169+
stickynessPolicy, stickynessPolicyNeedsUpdate, err := lb.checkStickynessPolicy(lbRule, service)
170+
if err != nil {
171+
return nil, err
172+
}
173+
if stickynessPolicyNeedsUpdate {
174+
if stickynessPolicy != nil {
175+
klog.V(4).Infof("Recreate stickyness policy: %v", lbRuleName)
176+
if err := lb.deleteStickynessPolicy(stickynessPolicy.Id); err != nil {
177+
return nil, err
178+
}
179+
delete(lb.stickynessPolicies, lbRule.Id)
180+
} else {
181+
klog.V(4).Infof("Creating stickyness policy: %v", lbRuleName)
182+
}
183+
if _, err := lb.createStickynessPolicy(lbRuleName, lbRule.Id, service); err != nil {
184+
return nil, err
185+
}
186+
// Remove from map to mark as handled (map tracks initial state for comparison)
187+
delete(lb.stickynessPolicies, lbRule.Id)
188+
}
164189
} else {
165190
klog.V(4).Infof("Creating load balancer rule: %v", lbRuleName)
166191
lbRule, err = lb.createLoadBalancerRule(lbRuleName, port, protocol, service)
167192
if err != nil {
168193
return nil, err
169194
}
195+
if _, err := lb.createStickynessPolicy(lbRuleName, lbRule.Id, service); err != nil {
196+
return nil, err
197+
}
170198

171199
klog.V(4).Infof("Assigning hosts (%v) to load balancer rule: %v", lb.hostIDs, lbRuleName)
172200
if err = lb.assignHostsToRule(lbRule, lb.hostIDs); err != nil {
@@ -372,10 +400,11 @@ func (cs *CSCloud) GetLoadBalancerName(ctx context.Context, clusterName string,
372400
// getLoadBalancer retrieves the IP address and ID and all the existing rules it can find.
373401
func (cs *CSCloud) getLoadBalancer(service *corev1.Service) (*loadBalancer, error) {
374402
lb := &loadBalancer{
375-
CloudStackClient: cs.client,
376-
name: cs.GetLoadBalancerName(context.TODO(), "", service),
377-
projectID: cs.projectID,
378-
rules: make(map[string]*cloudstack.LoadBalancerRule),
403+
CloudStackClient: cs.client,
404+
name: cs.GetLoadBalancerName(context.TODO(), "", service),
405+
projectID: cs.projectID,
406+
rules: make(map[string]*cloudstack.LoadBalancerRule),
407+
stickynessPolicies: make(map[string]*cloudstack.LBStickinessPolicyStickinesspolicy),
379408
}
380409

381410
p := cs.client.LoadBalancer.NewListLoadBalancerRulesParams()
@@ -400,6 +429,16 @@ func (cs *CSCloud) getLoadBalancer(service *corev1.Service) (*loadBalancer, erro
400429

401430
lb.ipAddr = lbRule.Publicip
402431
lb.ipAddrID = lbRule.Publicipid
432+
433+
lbStickinessPoliciesParams := cs.client.LoadBalancer.NewListLBStickinessPoliciesParams()
434+
lbStickinessPoliciesParams.SetLbruleid(lbRule.Id)
435+
lbStickinessPolicies, err := cs.client.LoadBalancer.ListLBStickinessPolicies(lbStickinessPoliciesParams)
436+
if err != nil {
437+
return nil, fmt.Errorf("error retrieving stickyness policies: %v", err)
438+
}
439+
if len(lbStickinessPolicies.LBStickinessPolicies) > 0 {
440+
lb.stickynessPolicies[lbRule.Id] = &lbStickinessPolicies.LBStickinessPolicies[0].Stickinesspolicy[0]
441+
}
403442
}
404443

405444
klog.V(4).Infof("Load balancer %v contains %d rule(s)", lb.name, len(lb.rules))
@@ -561,6 +600,60 @@ func (lb *loadBalancer) releaseLoadBalancerIP() error {
561600
return nil
562601
}
563602

603+
func (lb *loadBalancer) checkStickynessPolicy(lbRule *cloudstack.LoadBalancerRule, service *corev1.Service) (*cloudstack.LBStickinessPolicyStickinesspolicy, bool, error) {
604+
stickynessPolicy := lb.stickynessPolicies[lbRule.Id]
605+
stickynessMethodName := getStringFromServiceAnnotation(service, ServiceAnnotationLoadBalancerStickynessMethodName, "")
606+
stickynessMethodParam := getStringFromServiceAnnotation(service, ServiceAnnotationLoadBalancerStickynessParam, "")
607+
stickynessMethodParams := parseStickynessParams(stickynessMethodParam)
608+
609+
// If no policy exists and no method name is specified, no action needed
610+
if stickynessPolicy == nil {
611+
if stickynessMethodName == "" {
612+
return nil, false, nil
613+
}
614+
klog.V(4).Infof("sticky policy not found for rule: %v", lbRule.Name)
615+
return nil, true, nil
616+
}
617+
618+
// If policy exists but method name is not specified, policy should be deleted
619+
if stickynessMethodName == "" {
620+
klog.V(4).Infof("sticky policy exists but annotation removed for rule: %v", lbRule.Name)
621+
return stickynessPolicy, true, nil
622+
}
623+
624+
// Policy exists and method name is specified - check if it matches
625+
klog.V(4).Infof("sticky policy found for rule: %v", lbRule.Name)
626+
if stickynessPolicy.Methodname != stickynessMethodName {
627+
klog.V(4).Infof("sticky policy method name does not match: %v", lbRule.Name)
628+
return stickynessPolicy, true, nil
629+
}
630+
631+
// Check if params match
632+
if len(stickynessPolicy.Params) != len(stickynessMethodParams) {
633+
klog.V(4).Infof("sticky policy params length does not match: %v", lbRule.Name)
634+
return stickynessPolicy, true, nil
635+
}
636+
637+
// Check if all keys in stickynessPolicy.Params match stickynessMethodParams
638+
for key, value := range stickynessPolicy.Params {
639+
if stickynessMethodParams[key] != value {
640+
klog.V(4).Infof("sticky policy param %v does not match: %v", key, value)
641+
return stickynessPolicy, true, nil
642+
}
643+
}
644+
645+
// Check if all keys in stickynessMethodParams exist in stickynessPolicy.Params
646+
for key := range stickynessMethodParams {
647+
if _, exists := stickynessPolicy.Params[key]; !exists {
648+
klog.V(4).Infof("sticky policy missing param: %v", key)
649+
return stickynessPolicy, true, nil
650+
}
651+
}
652+
653+
// Policy matches desired state
654+
return stickynessPolicy, false, nil
655+
}
656+
564657
// checkLoadBalancerRule checks if the rule already exists and if it does, if it can be updated. If
565658
// it does exist but cannot be updated, it will delete the existing rule so it can be created again.
566659
func (lb *loadBalancer) checkLoadBalancerRule(lbRuleName string, port corev1.ServicePort, protocol LoadBalancerProtocol) (*cloudstack.LoadBalancerRule, bool, error) {
@@ -596,6 +689,43 @@ func (lb *loadBalancer) updateLoadBalancerRule(lbRuleName string, protocol LoadB
596689
return err
597690
}
598691

692+
// createStickynessPolicy creates a new stickyness policy and returns it.
693+
func (lb *loadBalancer) createStickynessPolicy(lbRuleName string, lbRuleId string, service *corev1.Service) (*cloudstack.LBStickinessPolicyStickinesspolicy, error) {
694+
stickynessMethodName := getStringFromServiceAnnotation(service, ServiceAnnotationLoadBalancerStickynessMethodName, "")
695+
stickynessMethodParam := getStringFromServiceAnnotation(service, ServiceAnnotationLoadBalancerStickynessParam, "")
696+
// If the stickyness method name is not set, we don't need to create a stickyness policy.
697+
if stickynessMethodName == "" {
698+
return nil, nil
699+
}
700+
p := lb.LoadBalancer.NewCreateLBStickinessPolicyParams(lbRuleId, stickynessMethodName, lbRuleName)
701+
702+
params := parseStickynessParams(stickynessMethodParam)
703+
p.SetParam(params)
704+
705+
stickynessPolicy, err := lb.LoadBalancer.CreateLBStickinessPolicy(p)
706+
if err != nil {
707+
return nil, fmt.Errorf("error creating stickyness policy: %v", err)
708+
}
709+
// return &stickynessPolicy.Stickinesspolicy[0].Stickinesspolicy, nil
710+
return &cloudstack.LBStickinessPolicyStickinesspolicy{
711+
Methodname: stickynessPolicy.Stickinesspolicy[0].Methodname,
712+
Params: stickynessPolicy.Stickinesspolicy[0].Params,
713+
Id: stickynessPolicy.Stickinesspolicy[0].Id,
714+
Name: stickynessPolicy.Stickinesspolicy[0].Name,
715+
State: stickynessPolicy.Stickinesspolicy[0].State,
716+
}, nil
717+
}
718+
719+
// deleteStickynessPolicy deletes a stickyness policy.
720+
func (lb *loadBalancer) deleteStickynessPolicy(stickynessPolicyId string) error {
721+
p := lb.LoadBalancer.NewDeleteLBStickinessPolicyParams(stickynessPolicyId)
722+
723+
if _, err := lb.LoadBalancer.DeleteLBStickinessPolicy(p); err != nil {
724+
return fmt.Errorf("error deleting stickyness policy %v: %v", stickynessPolicyId, err)
725+
}
726+
return nil
727+
}
728+
599729
// createLoadBalancerRule creates a new load balancer rule and returns it's ID.
600730
func (lb *loadBalancer) createLoadBalancerRule(lbRuleName string, port corev1.ServicePort, protocol LoadBalancerProtocol, service *corev1.Service) (*cloudstack.LoadBalancerRule, error) {
601731
p := lb.LoadBalancer.NewCreateLoadBalancerRuleParams(
@@ -663,6 +793,7 @@ func (lb *loadBalancer) deleteLoadBalancerRule(lbRule *cloudstack.LoadBalancerRu
663793

664794
// Delete the rule from the map as it no longer exists
665795
delete(lb.rules, lbRule.Name)
796+
delete(lb.stickynessPolicies, lbRule.Id)
666797

667798
return nil
668799
}
@@ -1027,6 +1158,23 @@ func getStringFromServiceAnnotation(service *corev1.Service, annotationKey strin
10271158
return defaultSetting
10281159
}
10291160

1161+
// parseStickynessParams parses a comma-separated string of key=value pairs into a map.
1162+
// Empty values and malformed entries are ignored.
1163+
func parseStickynessParams(paramString string) map[string]string {
1164+
params := make(map[string]string)
1165+
for _, param := range strings.Split(paramString, ",") {
1166+
param = strings.TrimSpace(param)
1167+
if param == "" {
1168+
continue
1169+
}
1170+
parts := strings.SplitN(param, "=", 2)
1171+
if len(parts) == 2 {
1172+
params[parts[0]] = parts[1]
1173+
}
1174+
}
1175+
return params
1176+
}
1177+
10301178
// getBoolFromServiceAnnotation searches a given v1.Service for a specific annotationKey and either returns the annotation's boolean value or a specified defaultSetting
10311179
func getBoolFromServiceAnnotation(service *corev1.Service, annotationKey string, defaultSetting bool) bool {
10321180
klog.V(4).Infof("getBoolFromServiceAnnotation(%s/%s, %v, %v)", service.Namespace, service.Name, annotationKey, defaultSetting)

0 commit comments

Comments
 (0)