This is an automated email from the ASF dual-hosted git repository.

vishesh pushed a commit to branch feat-add-stickyness-policy
in repository 
https://gitbox.apache.org/repos/asf/cloudstack-kubernetes-provider.git

commit f25a1e7c86f00394689d790071d86aeb969cf31c
Author: vishesh92 <[email protected]>
AuthorDate: Tue Dec 2 14:26:35 2025 +0530

    Feat: Add support for stickyness policy
---
 cloudstack_loadbalancer.go | 172 +++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 160 insertions(+), 12 deletions(-)

diff --git a/cloudstack_loadbalancer.go b/cloudstack_loadbalancer.go
index 00f536f0..49cd0425 100644
--- a/cloudstack_loadbalancer.go
+++ b/cloudstack_loadbalancer.go
@@ -45,19 +45,23 @@ const (
        ServiceAnnotationLoadBalancerProxyProtocol        = 
"service.beta.kubernetes.io/cloudstack-load-balancer-proxy-protocol"
        ServiceAnnotationLoadBalancerLoadbalancerHostname = 
"service.beta.kubernetes.io/cloudstack-load-balancer-hostname"
        ServiceAnnotationLoadBalancerSourceCidrs          = 
"service.beta.kubernetes.io/cloudstack-load-balancer-source-cidrs"
+
+       ServiceAnnotationLoadBalancerStickynessMethodName = 
"service.beta.kubernetes.io/cloudstack-load-balancer-stickyness-method-name"
+       ServiceAnnotationLoadBalancerStickynessParam      = 
"service.beta.kubernetes.io/cloudstack-load-balancer-stickyness-method-param"
 )
 
 type loadBalancer struct {
        *cloudstack.CloudStackClient
 
-       name      string
-       algorithm string
-       hostIDs   []string
-       ipAddr    string
-       ipAddrID  string
-       networkID string
-       projectID string
-       rules     map[string]*cloudstack.LoadBalancerRule
+       name               string
+       algorithm          string
+       hostIDs            []string
+       ipAddr             string
+       ipAddrID           string
+       networkID          string
+       projectID          string
+       rules              map[string]*cloudstack.LoadBalancerRule
+       stickynessPolicies 
map[string]*cloudstack.LBStickinessPolicyStickinesspolicy
 }
 
 // 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
                                // Delete the rule from the map, to prevent it 
being deleted.
                                delete(lb.rules, lbRuleName)
                        }
+
+                       stickynessPolicy, stickynessPolicyNeedsUpdate, err := 
lb.checkStickynessPolicy(lbRule, service)
+                       if err != nil {
+                               return nil, err
+                       }
+                       if stickynessPolicyNeedsUpdate {
+                               if stickynessPolicy != nil {
+                                       klog.V(4).Infof("Recreate stickyness 
policy: %v", lbRuleName)
+                                       if err := 
lb.deleteStickynessPolicy(stickynessPolicy.Id); err != nil {
+                                               return nil, err
+                                       }
+                                       delete(lb.stickynessPolicies, lbRule.Id)
+                               } else {
+                                       klog.V(4).Infof("Creating stickyness 
policy: %v", lbRuleName)
+                               }
+                               if _, err := 
lb.createStickynessPolicy(lbRuleName, lbRule.Id, service); err != nil {
+                                       return nil, err
+                               }
+                               // Remove from map to mark as handled (map 
tracks initial state for comparison)
+                               delete(lb.stickynessPolicies, lbRule.Id)
+                       }
                } else {
                        klog.V(4).Infof("Creating load balancer rule: %v", 
lbRuleName)
                        lbRule, err = lb.createLoadBalancerRule(lbRuleName, 
port, protocol, service)
                        if err != nil {
                                return nil, err
                        }
+                       if _, err := lb.createStickynessPolicy(lbRuleName, 
lbRule.Id, service); err != nil {
+                               return nil, err
+                       }
 
                        klog.V(4).Infof("Assigning hosts (%v) to load balancer 
rule: %v", lb.hostIDs, lbRuleName)
                        if err = lb.assignHostsToRule(lbRule, lb.hostIDs); err 
!= nil {
@@ -372,10 +400,11 @@ func (cs *CSCloud) GetLoadBalancerName(ctx 
context.Context, clusterName string,
 // getLoadBalancer retrieves the IP address and ID and all the existing rules 
it can find.
 func (cs *CSCloud) getLoadBalancer(service *corev1.Service) (*loadBalancer, 
error) {
        lb := &loadBalancer{
-               CloudStackClient: cs.client,
-               name:             cs.GetLoadBalancerName(context.TODO(), "", 
service),
-               projectID:        cs.projectID,
-               rules:            make(map[string]*cloudstack.LoadBalancerRule),
+               CloudStackClient:   cs.client,
+               name:               cs.GetLoadBalancerName(context.TODO(), "", 
service),
+               projectID:          cs.projectID,
+               rules:              
make(map[string]*cloudstack.LoadBalancerRule),
+               stickynessPolicies: 
make(map[string]*cloudstack.LBStickinessPolicyStickinesspolicy),
        }
 
        p := cs.client.LoadBalancer.NewListLoadBalancerRulesParams()
@@ -400,6 +429,16 @@ func (cs *CSCloud) getLoadBalancer(service 
*corev1.Service) (*loadBalancer, erro
 
                lb.ipAddr = lbRule.Publicip
                lb.ipAddrID = lbRule.Publicipid
+
+               lbStickinessPoliciesParams := 
cs.client.LoadBalancer.NewListLBStickinessPoliciesParams()
+               lbStickinessPoliciesParams.SetLbruleid(lbRule.Id)
+               lbStickinessPolicies, err := 
cs.client.LoadBalancer.ListLBStickinessPolicies(lbStickinessPoliciesParams)
+               if err != nil {
+                       return nil, fmt.Errorf("error retrieving stickyness 
policies: %v", err)
+               }
+               if len(lbStickinessPolicies.LBStickinessPolicies) > 0 {
+                       lb.stickynessPolicies[lbRule.Id] = 
&lbStickinessPolicies.LBStickinessPolicies[0].Stickinesspolicy[0]
+               }
        }
 
        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 {
        return nil
 }
 
+func (lb *loadBalancer) checkStickynessPolicy(lbRule 
*cloudstack.LoadBalancerRule, service *corev1.Service) 
(*cloudstack.LBStickinessPolicyStickinesspolicy, bool, error) {
+       stickynessPolicy := lb.stickynessPolicies[lbRule.Id]
+       stickynessMethodName := getStringFromServiceAnnotation(service, 
ServiceAnnotationLoadBalancerStickynessMethodName, "")
+       stickynessMethodParam := getStringFromServiceAnnotation(service, 
ServiceAnnotationLoadBalancerStickynessParam, "")
+       stickynessMethodParams := parseStickynessParams(stickynessMethodParam)
+
+       // If no policy exists and no method name is specified, no action needed
+       if stickynessPolicy == nil {
+               if stickynessMethodName == "" {
+                       return nil, false, nil
+               }
+               klog.V(4).Infof("sticky policy not found for rule: %v", 
lbRule.Name)
+               return nil, true, nil
+       }
+
+       // If policy exists but method name is not specified, policy should be 
deleted
+       if stickynessMethodName == "" {
+               klog.V(4).Infof("sticky policy exists but annotation removed 
for rule: %v", lbRule.Name)
+               return stickynessPolicy, true, nil
+       }
+
+       // Policy exists and method name is specified - check if it matches
+       klog.V(4).Infof("sticky policy found for rule: %v", lbRule.Name)
+       if stickynessPolicy.Methodname != stickynessMethodName {
+               klog.V(4).Infof("sticky policy method name does not match: %v", 
lbRule.Name)
+               return stickynessPolicy, true, nil
+       }
+
+       // Check if params match
+       if len(stickynessPolicy.Params) != len(stickynessMethodParams) {
+               klog.V(4).Infof("sticky policy params length does not match: 
%v", lbRule.Name)
+               return stickynessPolicy, true, nil
+       }
+
+       // Check if all keys in stickynessPolicy.Params match 
stickynessMethodParams
+       for key, value := range stickynessPolicy.Params {
+               if stickynessMethodParams[key] != value {
+                       klog.V(4).Infof("sticky policy param %v does not match: 
%v", key, value)
+                       return stickynessPolicy, true, nil
+               }
+       }
+
+       // Check if all keys in stickynessMethodParams exist in 
stickynessPolicy.Params
+       for key := range stickynessMethodParams {
+               if _, exists := stickynessPolicy.Params[key]; !exists {
+                       klog.V(4).Infof("sticky policy missing param: %v", key)
+                       return stickynessPolicy, true, nil
+               }
+       }
+
+       // Policy matches desired state
+       return stickynessPolicy, false, nil
+}
+
 // checkLoadBalancerRule checks if the rule already exists and if it does, if 
it can be updated. If
 // it does exist but cannot be updated, it will delete the existing rule so it 
can be created again.
 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
        return err
 }
 
+// createStickynessPolicy creates a new stickyness policy and returns it.
+func (lb *loadBalancer) createStickynessPolicy(lbRuleName string, lbRuleId 
string, service *corev1.Service) 
(*cloudstack.LBStickinessPolicyStickinesspolicy, error) {
+       stickynessMethodName := getStringFromServiceAnnotation(service, 
ServiceAnnotationLoadBalancerStickynessMethodName, "")
+       stickynessMethodParam := getStringFromServiceAnnotation(service, 
ServiceAnnotationLoadBalancerStickynessParam, "")
+       // If the stickyness method name is not set, we don't need to create a 
stickyness policy.
+       if stickynessMethodName == "" {
+               return nil, nil
+       }
+       p := lb.LoadBalancer.NewCreateLBStickinessPolicyParams(lbRuleId, 
stickynessMethodName, lbRuleName)
+
+       params := parseStickynessParams(stickynessMethodParam)
+       p.SetParam(params)
+
+       stickynessPolicy, err := lb.LoadBalancer.CreateLBStickinessPolicy(p)
+       if err != nil {
+               return nil, fmt.Errorf("error creating stickyness policy: %v", 
err)
+       }
+       // return &stickynessPolicy.Stickinesspolicy[0].Stickinesspolicy, nil
+       return &cloudstack.LBStickinessPolicyStickinesspolicy{
+               Methodname: stickynessPolicy.Stickinesspolicy[0].Methodname,
+               Params:     stickynessPolicy.Stickinesspolicy[0].Params,
+               Id:         stickynessPolicy.Stickinesspolicy[0].Id,
+               Name:       stickynessPolicy.Stickinesspolicy[0].Name,
+               State:      stickynessPolicy.Stickinesspolicy[0].State,
+       }, nil
+}
+
+// deleteStickynessPolicy deletes a stickyness policy.
+func (lb *loadBalancer) deleteStickynessPolicy(stickynessPolicyId string) 
error {
+       p := 
lb.LoadBalancer.NewDeleteLBStickinessPolicyParams(stickynessPolicyId)
+
+       if _, err := lb.LoadBalancer.DeleteLBStickinessPolicy(p); err != nil {
+               return fmt.Errorf("error deleting stickyness policy %v: %v", 
stickynessPolicyId, err)
+       }
+       return nil
+}
+
 // createLoadBalancerRule creates a new load balancer rule and returns it's ID.
 func (lb *loadBalancer) createLoadBalancerRule(lbRuleName string, port 
corev1.ServicePort, protocol LoadBalancerProtocol, service *corev1.Service) 
(*cloudstack.LoadBalancerRule, error) {
        p := lb.LoadBalancer.NewCreateLoadBalancerRuleParams(
@@ -663,6 +793,7 @@ func (lb *loadBalancer) deleteLoadBalancerRule(lbRule 
*cloudstack.LoadBalancerRu
 
        // Delete the rule from the map as it no longer exists
        delete(lb.rules, lbRule.Name)
+       delete(lb.stickynessPolicies, lbRule.Id)
 
        return nil
 }
@@ -1027,6 +1158,23 @@ func getStringFromServiceAnnotation(service 
*corev1.Service, annotationKey strin
        return defaultSetting
 }
 
+// parseStickynessParams parses a comma-separated string of key=value pairs 
into a map.
+// Empty values and malformed entries are ignored.
+func parseStickynessParams(paramString string) map[string]string {
+       params := make(map[string]string)
+       for _, param := range strings.Split(paramString, ",") {
+               param = strings.TrimSpace(param)
+               if param == "" {
+                       continue
+               }
+               parts := strings.SplitN(param, "=", 2)
+               if len(parts) == 2 {
+                       params[parts[0]] = parts[1]
+               }
+       }
+       return params
+}
+
 // getBoolFromServiceAnnotation searches a given v1.Service for a specific 
annotationKey and either returns the annotation's boolean value or a specified 
defaultSetting
 func getBoolFromServiceAnnotation(service *corev1.Service, annotationKey 
string, defaultSetting bool) bool {
        klog.V(4).Infof("getBoolFromServiceAnnotation(%s/%s, %v, %v)", 
service.Namespace, service.Name, annotationKey, defaultSetting)

Reply via email to