@@ -56,6 +56,9 @@ const (
5656 // associated the IP address. This annotation is set by the controller when it associates
5757 // an unallocated IP, and is used to determine if the IP should be disassociated on deletion.
5858 ServiceAnnotationLoadBalancerIPAssociatedByController = "service.beta.kubernetes.io/cloudstack-load-balancer-ip-associated-by-controller" //nolint:gosec
59+
60+ ServiceAnnotationLoadBalancerStickynessMethodName = "service.beta.kubernetes.io/cloudstack-load-balancer-stickyness-method-name"
61+ ServiceAnnotationLoadBalancerStickynessParam = "service.beta.kubernetes.io/cloudstack-load-balancer-stickyness-method-param"
5962)
6063
6164type loadBalancer struct {
@@ -69,6 +72,7 @@ type loadBalancer struct {
6972 networkID string
7073 projectID string
7174 rules map [string ]* cloudstack.LoadBalancerRule
75+ stickynessPolicies map [string ]* cloudstack.LBStickinessPolicyStickinesspolicy
7276 ipAssociatedByController bool
7377}
7478
@@ -181,12 +185,36 @@ func (cs *CSCloud) EnsureLoadBalancer(ctx context.Context, clusterName string, s
181185 // Delete the rule from the map, to prevent it being deleted.
182186 delete (lb .rules , lbRuleName )
183187 }
188+
189+ stickynessPolicy , stickynessPolicyNeedsUpdate , err := lb .checkStickynessPolicy (lbRule , service )
190+ if err != nil {
191+ return nil , err
192+ }
193+ if stickynessPolicyNeedsUpdate {
194+ if stickynessPolicy != nil {
195+ klog .V (4 ).Infof ("Recreate stickyness policy: %v" , lbRuleName )
196+ if err := lb .deleteStickynessPolicy (stickynessPolicy .Id ); err != nil {
197+ return nil , err
198+ }
199+ delete (lb .stickynessPolicies , lbRule .Id )
200+ } else {
201+ klog .V (4 ).Infof ("Creating stickyness policy: %v" , lbRuleName )
202+ }
203+ if _ , err := lb .createStickynessPolicy (lbRuleName , lbRule .Id , service ); err != nil {
204+ return nil , err
205+ }
206+ // Remove from map to mark as handled (map tracks initial state for comparison)
207+ delete (lb .stickynessPolicies , lbRule .Id )
208+ }
184209 } else {
185210 klog .V (4 ).Infof ("Creating load balancer rule: %v" , lbRuleName )
186211 lbRule , err = lb .createLoadBalancerRule (lbRuleName , port , protocol , service )
187212 if err != nil {
188213 return nil , err
189214 }
215+ if _ , err := lb .createStickynessPolicy (lbRuleName , lbRule .Id , service ); err != nil {
216+ return nil , err
217+ }
190218
191219 klog .V (4 ).Infof ("Assigning hosts (%v) to load balancer rule: %v" , lb .hostIDs , lbRuleName )
192220 if err = lb .assignHostsToRule (lbRule , lb .hostIDs ); err != nil {
@@ -434,10 +462,11 @@ func (cs *CSCloud) GetLoadBalancerName(ctx context.Context, clusterName string,
434462// getLoadBalancer retrieves the IP address and ID and all the existing rules it can find.
435463func (cs * CSCloud ) getLoadBalancer (service * corev1.Service ) (* loadBalancer , error ) {
436464 lb := & loadBalancer {
437- CloudStackClient : cs .client ,
438- name : cs .GetLoadBalancerName (context .TODO (), "" , service ),
439- projectID : cs .projectID ,
440- rules : make (map [string ]* cloudstack.LoadBalancerRule ),
465+ CloudStackClient : cs .client ,
466+ name : cs .GetLoadBalancerName (context .TODO (), "" , service ),
467+ projectID : cs .projectID ,
468+ rules : make (map [string ]* cloudstack.LoadBalancerRule ),
469+ stickynessPolicies : make (map [string ]* cloudstack.LBStickinessPolicyStickinesspolicy ),
441470 }
442471
443472 p := cs .client .LoadBalancer .NewListLoadBalancerRulesParams ()
@@ -462,6 +491,16 @@ func (cs *CSCloud) getLoadBalancer(service *corev1.Service) (*loadBalancer, erro
462491
463492 lb .ipAddr = lbRule .Publicip
464493 lb .ipAddrID = lbRule .Publicipid
494+
495+ lbStickinessPoliciesParams := cs .client .LoadBalancer .NewListLBStickinessPoliciesParams ()
496+ lbStickinessPoliciesParams .SetLbruleid (lbRule .Id )
497+ lbStickinessPolicies , err := cs .client .LoadBalancer .ListLBStickinessPolicies (lbStickinessPoliciesParams )
498+ if err != nil {
499+ return nil , fmt .Errorf ("error retrieving stickyness policies: %v" , err )
500+ }
501+ if len (lbStickinessPolicies .LBStickinessPolicies ) > 0 {
502+ lb .stickynessPolicies [lbRule .Id ] = & lbStickinessPolicies .LBStickinessPolicies [0 ].Stickinesspolicy [0 ]
503+ }
465504 }
466505
467506 klog .V (4 ).Infof ("Load balancer %v contains %d rule(s)" , lb .name , len (lb .rules ))
@@ -649,6 +688,60 @@ func (lb *loadBalancer) getCIDRList(service *corev1.Service) ([]string, error) {
649688 return cidrList , nil
650689}
651690
691+ func (lb * loadBalancer ) checkStickynessPolicy (lbRule * cloudstack.LoadBalancerRule , service * corev1.Service ) (* cloudstack.LBStickinessPolicyStickinesspolicy , bool , error ) {
692+ stickynessPolicy := lb .stickynessPolicies [lbRule .Id ]
693+ stickynessMethodName := getStringFromServiceAnnotation (service , ServiceAnnotationLoadBalancerStickynessMethodName , "" )
694+ stickynessMethodParam := getStringFromServiceAnnotation (service , ServiceAnnotationLoadBalancerStickynessParam , "" )
695+ stickynessMethodParams := parseStickynessParams (stickynessMethodParam )
696+
697+ // If no policy exists and no method name is specified, no action needed
698+ if stickynessPolicy == nil {
699+ if stickynessMethodName == "" {
700+ return nil , false , nil
701+ }
702+ klog .V (4 ).Infof ("sticky policy not found for rule: %v" , lbRule .Name )
703+ return nil , true , nil
704+ }
705+
706+ // If policy exists but method name is not specified, policy should be deleted
707+ if stickynessMethodName == "" {
708+ klog .V (4 ).Infof ("sticky policy exists but annotation removed for rule: %v" , lbRule .Name )
709+ return stickynessPolicy , true , nil
710+ }
711+
712+ // Policy exists and method name is specified - check if it matches
713+ klog .V (4 ).Infof ("sticky policy found for rule: %v" , lbRule .Name )
714+ if stickynessPolicy .Methodname != stickynessMethodName {
715+ klog .V (4 ).Infof ("sticky policy method name does not match: %v" , lbRule .Name )
716+ return stickynessPolicy , true , nil
717+ }
718+
719+ // Check if params match
720+ if len (stickynessPolicy .Params ) != len (stickynessMethodParams ) {
721+ klog .V (4 ).Infof ("sticky policy params length does not match: %v" , lbRule .Name )
722+ return stickynessPolicy , true , nil
723+ }
724+
725+ // Check if all keys in stickynessPolicy.Params match stickynessMethodParams
726+ for key , value := range stickynessPolicy .Params {
727+ if stickynessMethodParams [key ] != value {
728+ klog .V (4 ).Infof ("sticky policy param %v does not match: %v" , key , value )
729+ return stickynessPolicy , true , nil
730+ }
731+ }
732+
733+ // Check if all keys in stickynessMethodParams exist in stickynessPolicy.Params
734+ for key := range stickynessMethodParams {
735+ if _ , exists := stickynessPolicy .Params [key ]; ! exists {
736+ klog .V (4 ).Infof ("sticky policy missing param: %v" , key )
737+ return stickynessPolicy , true , nil
738+ }
739+ }
740+
741+ // Policy matches desired state
742+ return stickynessPolicy , false , nil
743+ }
744+
652745// checkLoadBalancerRule checks if the rule already exists and if it does, if it can be updated. If
653746// it does exist but cannot be updated, it will delete the existing rule so it can be created again.
654747func (lb * loadBalancer ) checkLoadBalancerRule (lbRuleName string , port corev1.ServicePort , protocol LoadBalancerProtocol , service * corev1.Service , version semver.Version ) (* cloudstack.LoadBalancerRule , bool , error ) {
@@ -715,6 +808,43 @@ func (lb *loadBalancer) updateLoadBalancerRule(lbRuleName string, protocol LoadB
715808 return err
716809}
717810
811+ // createStickynessPolicy creates a new stickyness policy and returns it.
812+ func (lb * loadBalancer ) createStickynessPolicy (lbRuleName string , lbRuleId string , service * corev1.Service ) (* cloudstack.LBStickinessPolicyStickinesspolicy , error ) {
813+ stickynessMethodName := getStringFromServiceAnnotation (service , ServiceAnnotationLoadBalancerStickynessMethodName , "" )
814+ stickynessMethodParam := getStringFromServiceAnnotation (service , ServiceAnnotationLoadBalancerStickynessParam , "" )
815+ // If the stickyness method name is not set, we don't need to create a stickyness policy.
816+ if stickynessMethodName == "" {
817+ return nil , nil
818+ }
819+ p := lb .LoadBalancer .NewCreateLBStickinessPolicyParams (lbRuleId , stickynessMethodName , lbRuleName )
820+
821+ params := parseStickynessParams (stickynessMethodParam )
822+ p .SetParam (params )
823+
824+ stickynessPolicy , err := lb .LoadBalancer .CreateLBStickinessPolicy (p )
825+ if err != nil {
826+ return nil , fmt .Errorf ("error creating stickyness policy: %v" , err )
827+ }
828+ // return &stickynessPolicy.Stickinesspolicy[0].Stickinesspolicy, nil
829+ return & cloudstack.LBStickinessPolicyStickinesspolicy {
830+ Methodname : stickynessPolicy .Stickinesspolicy [0 ].Methodname ,
831+ Params : stickynessPolicy .Stickinesspolicy [0 ].Params ,
832+ Id : stickynessPolicy .Stickinesspolicy [0 ].Id ,
833+ Name : stickynessPolicy .Stickinesspolicy [0 ].Name ,
834+ State : stickynessPolicy .Stickinesspolicy [0 ].State ,
835+ }, nil
836+ }
837+
838+ // deleteStickynessPolicy deletes a stickyness policy.
839+ func (lb * loadBalancer ) deleteStickynessPolicy (stickynessPolicyId string ) error {
840+ p := lb .LoadBalancer .NewDeleteLBStickinessPolicyParams (stickynessPolicyId )
841+
842+ if _ , err := lb .LoadBalancer .DeleteLBStickinessPolicy (p ); err != nil {
843+ return fmt .Errorf ("error deleting stickyness policy %v: %v" , stickynessPolicyId , err )
844+ }
845+ return nil
846+ }
847+
718848// createLoadBalancerRule creates a new load balancer rule and returns it's ID.
719849func (lb * loadBalancer ) createLoadBalancerRule (lbRuleName string , port corev1.ServicePort , protocol LoadBalancerProtocol , service * corev1.Service ) (* cloudstack.LoadBalancerRule , error ) {
720850 p := lb .LoadBalancer .NewCreateLoadBalancerRuleParams (
@@ -772,6 +902,7 @@ func (lb *loadBalancer) deleteLoadBalancerRule(lbRule *cloudstack.LoadBalancerRu
772902
773903 // Delete the rule from the map as it no longer exists
774904 delete (lb .rules , lbRule .Name )
905+ delete (lb .stickynessPolicies , lbRule .Id )
775906
776907 return nil
777908}
@@ -1136,6 +1267,23 @@ func getStringFromServiceAnnotation(service *corev1.Service, annotationKey strin
11361267 return defaultSetting
11371268}
11381269
1270+ // parseStickynessParams parses a comma-separated string of key=value pairs into a map.
1271+ // Empty values and malformed entries are ignored.
1272+ func parseStickynessParams (paramString string ) map [string ]string {
1273+ params := make (map [string ]string )
1274+ for _ , param := range strings .Split (paramString , "," ) {
1275+ param = strings .TrimSpace (param )
1276+ if param == "" {
1277+ continue
1278+ }
1279+ parts := strings .SplitN (param , "=" , 2 )
1280+ if len (parts ) == 2 {
1281+ params [parts [0 ]] = parts [1 ]
1282+ }
1283+ }
1284+ return params
1285+ }
1286+
11391287// getBoolFromServiceAnnotation searches a given v1.Service for a specific annotationKey and either returns the annotation's boolean value or a specified defaultSetting
11401288func getBoolFromServiceAnnotation (service * corev1.Service , annotationKey string , defaultSetting bool ) bool {
11411289 klog .V (4 ).Infof ("getBoolFromServiceAnnotation(%s/%s, %v, %v)" , service .Namespace , service .Name , annotationKey , defaultSetting )
0 commit comments