@@ -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
5053type 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.
373401func (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.
566659func (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.
600730func (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
10311179func 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