Skip to content

Commit 207f378

Browse files
committed
Feat: Add support for stickyness policy
1 parent 1b75921 commit 207f378

File tree

1 file changed

+152
-4
lines changed

1 file changed

+152
-4
lines changed

cloudstack_loadbalancer.go

Lines changed: 152 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -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

6164
type 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.
435463
func (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.
654747
func (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.
719849
func (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
11401288
func 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

Comments
 (0)