Skip to content

Commit 0a8c733

Browse files
hrakclaude
andcommitted
feat: Add load-balancer-id and network-id annotations with ID-based lookup
Store the CloudStack public IP UUID and network UUID as service annotations so that subsequent reconciliation loops can look up load balancer rules by exact ID instead of relying on keyword-based LIKE %keyword% matching. The new getLoadBalancer orchestrator tries ID-based lookup first and falls back to name-based search when annotations are absent or stale. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 7873ce1 commit 0a8c733

File tree

2 files changed

+477
-5
lines changed

2 files changed

+477
-5
lines changed

cloudstack/cloudstack_loadbalancer.go

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,14 @@ const (
5858
// prevents the public IP from being released when the service is deleted.
5959
ServiceAnnotationLoadBalancerKeepIP = "service.beta.kubernetes.io/cloudstack-load-balancer-keep-ip"
6060

61+
// ServiceAnnotationLoadBalancerID stores the CloudStack public IP UUID associated with the load balancer.
62+
// Used for efficient ID-based lookups instead of keyword-based searches.
63+
ServiceAnnotationLoadBalancerID = "service.beta.kubernetes.io/cloudstack-load-balancer-id"
64+
65+
// ServiceAnnotationLoadBalancerNetworkID stores the CloudStack network UUID associated with the load balancer.
66+
// Used together with ServiceAnnotationLoadBalancerID for scoped ID-based lookups.
67+
ServiceAnnotationLoadBalancerNetworkID = "service.beta.kubernetes.io/cloudstack-load-balancer-network-id"
68+
6169
// Used to construct the load balancer name.
6270
servicePrefix = "K8s_svc_"
6371
lbNameFormat = "%s%s_%s_%s"
@@ -83,7 +91,7 @@ func (cs *CSCloud) GetLoadBalancer(ctx context.Context, clusterName string, serv
8391
// Get the load balancer details and existing rules.
8492
name := cs.GetLoadBalancerName(ctx, clusterName, service)
8593
legacyName := cs.getLoadBalancerLegacyName(ctx, clusterName, service)
86-
lb, err := cs.getLoadBalancerByName(name, legacyName)
94+
lb, err := cs.getLoadBalancer(service, name, legacyName)
8795
if err != nil {
8896
return nil, false, err
8997
}
@@ -117,7 +125,7 @@ func (cs *CSCloud) EnsureLoadBalancer(ctx context.Context, clusterName string, s
117125
// Get the load balancer details and existing rules.
118126
name := cs.GetLoadBalancerName(ctx, clusterName, service)
119127
legacyName := cs.getLoadBalancerLegacyName(ctx, clusterName, service)
120-
lb, err := cs.getLoadBalancerByName(name, legacyName)
128+
lb, err := cs.getLoadBalancer(service, name, legacyName)
121129
if err != nil {
122130
return nil, err
123131
}
@@ -175,8 +183,10 @@ func (cs *CSCloud) EnsureLoadBalancer(ctx context.Context, clusterName string, s
175183

176184
klog.V(4).Infof("Load balancer %v is associated with IP %v", lb.name, lb.ipAddr)
177185

178-
// Set the load balancer IP address annotation on the Service
186+
// Set the load balancer annotations on the Service
179187
setServiceAnnotation(service, ServiceAnnotationLoadBalancerAddress, lb.ipAddr)
188+
setServiceAnnotation(service, ServiceAnnotationLoadBalancerID, lb.ipAddrID)
189+
setServiceAnnotation(service, ServiceAnnotationLoadBalancerNetworkID, lb.networkID)
180190

181191
for _, port := range service.Spec.Ports {
182192
// Construct the protocol name first, we need it a few times
@@ -281,7 +291,7 @@ func (cs *CSCloud) UpdateLoadBalancer(ctx context.Context, clusterName string, s
281291
// Get the load balancer details and existing rules.
282292
name := cs.GetLoadBalancerName(ctx, clusterName, service)
283293
legacyName := cs.getLoadBalancerLegacyName(ctx, clusterName, service)
284-
lb, err := cs.getLoadBalancerByName(name, legacyName)
294+
lb, err := cs.getLoadBalancer(service, name, legacyName)
285295
if err != nil {
286296
return err
287297
}
@@ -320,7 +330,7 @@ func (cs *CSCloud) EnsureLoadBalancerDeleted(ctx context.Context, clusterName st
320330
// Get the load balancer details and existing rules.
321331
name := cs.GetLoadBalancerName(ctx, clusterName, service)
322332
legacyName := cs.getLoadBalancerLegacyName(ctx, clusterName, service)
323-
lb, err := cs.getLoadBalancerByName(name, legacyName)
333+
lb, err := cs.getLoadBalancer(service, name, legacyName)
324334
if err != nil {
325335
return err
326336
}
@@ -528,6 +538,28 @@ func filterRulesByPrefix(rules []*cloudstack.LoadBalancerRule, prefix string) []
528538
return filtered
529539
}
530540

541+
// getLoadBalancer tries to find the load balancer using ID-based lookup first (if annotations
542+
// are present), then falls back to the keyword-based name lookup.
543+
func (cs *CSCloud) getLoadBalancer(service *corev1.Service, name, legacyName string) (*loadBalancer, error) {
544+
if ipAddrID := getLoadBalancerID(service); ipAddrID != "" {
545+
networkID := getLoadBalancerNetworkID(service)
546+
klog.V(4).Infof("Attempting ID-based load balancer lookup: ipAddrID=%v, networkID=%v", ipAddrID, networkID)
547+
548+
lb, err := cs.getLoadBalancerByID(name, ipAddrID, networkID)
549+
if err != nil {
550+
return nil, err
551+
}
552+
553+
if len(lb.rules) > 0 {
554+
return lb, nil
555+
}
556+
557+
klog.V(4).Infof("ID-based lookup returned no rules, falling back to name-based lookup")
558+
}
559+
560+
return cs.getLoadBalancerByName(name, legacyName)
561+
}
562+
531563
// getLoadBalancerByName retrieves the IP address and ID and all the existing rules it can find.
532564
func (cs *CSCloud) getLoadBalancerByName(name, legacyName string) (*loadBalancer, error) {
533565
lb := &loadBalancer{
@@ -588,6 +620,50 @@ func (cs *CSCloud) getLoadBalancerByName(name, legacyName string) (*loadBalancer
588620
return lb, nil
589621
}
590622

623+
// getLoadBalancerByID retrieves load balancer rules by public IP ID and network ID.
624+
// This is more reliable than keyword-based search as it uses exact ID matching.
625+
func (cs *CSCloud) getLoadBalancerByID(name, ipAddrID, networkID string) (*loadBalancer, error) {
626+
lb := &loadBalancer{
627+
CloudStackClient: cs.client,
628+
name: name,
629+
projectID: cs.projectID,
630+
rules: make(map[string]*cloudstack.LoadBalancerRule),
631+
}
632+
633+
p := cs.client.LoadBalancer.NewListLoadBalancerRulesParams()
634+
p.SetPublicipid(ipAddrID)
635+
p.SetListall(true)
636+
637+
if networkID != "" {
638+
p.SetNetworkid(networkID)
639+
}
640+
641+
if cs.projectID != "" {
642+
p.SetProjectid(cs.projectID)
643+
}
644+
645+
l, err := cs.client.LoadBalancer.ListLoadBalancerRules(p)
646+
if err != nil {
647+
return nil, fmt.Errorf("error retrieving load balancer rules by IP ID %v: %w", ipAddrID, err)
648+
}
649+
650+
for _, lbRule := range l.LoadBalancerRules {
651+
lb.rules[lbRule.Name] = lbRule
652+
653+
if lb.ipAddr != "" && lb.ipAddr != lbRule.Publicip {
654+
klog.Warningf("Load balancer %v has rules associated with different IP's: %v, %v", lb.name, lb.ipAddr, lbRule.Publicip)
655+
}
656+
657+
lb.ipAddr = lbRule.Publicip
658+
lb.ipAddrID = lbRule.Publicipid
659+
lb.networkID = lbRule.Networkid
660+
}
661+
662+
klog.V(4).Infof("Load balancer %v (by ID %v) contains %d rule(s)", lb.name, ipAddrID, len(lb.rules))
663+
664+
return lb, nil
665+
}
666+
591667
// verifyHosts verifies if all hosts belong to the same network, and returns the host ID's and network ID.
592668
// During rolling upgrades some nodes may not yet have a corresponding VM in CloudStack, so we tolerate
593669
// partial matches: as long as at least one node can be resolved we return the matched set and log
@@ -1328,6 +1404,16 @@ func getLoadBalancerAddress(service *corev1.Service) string {
13281404
return service.Spec.LoadBalancerIP //nolint:staticcheck // deprecated but kept as fallback
13291405
}
13301406

1407+
// getLoadBalancerID returns the stored load balancer public IP UUID from the service annotation.
1408+
func getLoadBalancerID(service *corev1.Service) string {
1409+
return getStringFromServiceAnnotation(service, ServiceAnnotationLoadBalancerID, "")
1410+
}
1411+
1412+
// getLoadBalancerNetworkID returns the stored load balancer network UUID from the service annotation.
1413+
func getLoadBalancerNetworkID(service *corev1.Service) string {
1414+
return getStringFromServiceAnnotation(service, ServiceAnnotationLoadBalancerNetworkID, "")
1415+
}
1416+
13311417
// setServiceAnnotation is used to create/set or update an annotation on the Service object.
13321418
func setServiceAnnotation(service *corev1.Service, key, value string) {
13331419
if service.ObjectMeta.Annotations == nil {

0 commit comments

Comments
 (0)