@@ -2,18 +2,20 @@ package cloudscale_ccm
22
33import (
44 "context"
5- "errors"
65 "fmt"
76 "slices"
87 "strings"
98
10- "github.com/cloudscale-ch/cloudscale-cloud-controller-manager/pkg/internal/kubeutil"
119 "github.com/cloudscale-ch/cloudscale-go-sdk/v6"
1210 v1 "k8s.io/api/core/v1"
1311 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+ "k8s.io/apimachinery/pkg/labels"
1413 "k8s.io/client-go/kubernetes"
14+ "k8s.io/client-go/tools/record"
1515 "k8s.io/klog/v2"
1616 "k8s.io/utils/ptr"
17+
18+ "github.com/cloudscale-ch/cloudscale-cloud-controller-manager/pkg/internal/kubeutil"
1719)
1820
1921// Annotations used by the loadbalancer integration of cloudscale_ccm. Those
@@ -208,7 +210,7 @@ const (
208210 // connections timing out while the monitor is updated.
209211 LoadBalancerHealthMonitorTimeoutS = "k8s.cloudscale.ch/loadbalancer-health-monitor-timeout-s"
210212
211- // LoadBalancerHealthMonitorDownThreshold is the number of the checks that
213+ // LoadBalancerHealthMonitorUpThreshold is the number of the checks that
212214 // need to succeed before a pool member is considered up. Defaults to 2.
213215 LoadBalancerHealthMonitorUpThreshold = "k8s.cloudscale.ch/loadbalancer-health-monitor-up-threshold"
214216
@@ -278,7 +280,7 @@ const (
278280 // Changing this annotation on an established service is considered safe.
279281 LoadBalancerListenerTimeoutMemberDataMS = "k8s.cloudscale.ch/loadbalancer-timeout-member-data-ms"
280282
281- // LoadBalancerSubnetLimit is a JSON list of subnet UUIDs that the
283+ // LoadBalancerListenerAllowedSubnets is a JSON list of subnet UUIDs that the
282284 // loadbalancer should use. By default, all subnets of a node are used:
283285 //
284286 // * `[]` means that anyone is allowed to connect (default).
@@ -291,12 +293,21 @@ const (
291293 // This is an advanced feature, useful if you have nodes that are in
292294 // multiple private subnets.
293295 LoadBalancerListenerAllowedSubnets = "k8s.cloudscale.ch/loadbalancer-listener-allowed-subnets"
296+
297+ // LoadBalancerNodeSelector can be set to restrict which nodes are added to the LB pool.
298+ // It accepts a standard Kubernetes label selector string.
299+ //
300+ // N.B.: If the node-selector annotation doesn't match any nodes, the LoadBalancer will remove all members
301+ // from the LB pool, effectively causing a downtime on the LB.
302+ // Make sure to verify the node selector well before setting it.
303+ LoadBalancerNodeSelector = "k8s.cloudscale.ch/loadbalancer-node-selector"
294304)
295305
296306type loadbalancer struct {
297- lbs lbMapper
298- srv serverMapper
299- k8s kubernetes.Interface
307+ lbs lbMapper
308+ srv serverMapper
309+ k8s kubernetes.Interface
310+ recorder record.EventRecorder
300311}
301312
302313// GetLoadBalancer returns whether the specified load balancer exists, and
@@ -387,24 +398,34 @@ func (l *loadbalancer) EnsureLoadBalancer(
387398 return nil , err
388399 }
389400
390- // Refuse to do anything if there are no nodes
391- if len (nodes ) == 0 {
392- return nil , errors .New (
393- "no valid nodes for service found, please verify there is " +
394- "at least one that allows load balancers" ,
401+ filteredNodes , err := filterNodesBySelector (serviceInfo , nodes )
402+ if err != nil {
403+ return nil , err
404+ }
405+
406+ if len (filteredNodes ) == 0 {
407+ l .recorder .Event (
408+ service ,
409+ v1 .EventTypeWarning ,
410+ "NoValidNodes" ,
411+ fmt .Sprintf ("No valid nodes for service found, " +
412+ "double-check node-selector annotation: %s: %s" ,
413+ LoadBalancerNodeSelector ,
414+ serviceInfo .annotation (LoadBalancerNodeSelector ),
415+ ),
395416 )
396417 }
397418
398419 // Reconcile
399- err : = reconcileLbState (ctx , l .lbs .client , func () (* lbState , error ) {
420+ err = reconcileLbState (ctx , l .lbs .client , func () (* lbState , error ) {
400421 // Get the desired state from Kubernetes
401- servers , err := l .srv .mapNodes (ctx , nodes ).All ()
422+ servers , err := l .srv .mapNodes (ctx , filteredNodes ).All ()
402423 if err != nil {
403424 return nil , fmt .Errorf (
404425 "unable to get load balancer for %s: %w" , service .Name , err )
405426 }
406427
407- return desiredLbState (serviceInfo , nodes , servers )
428+ return desiredLbState (serviceInfo , filteredNodes , servers )
408429 }, func () (* lbState , error ) {
409430 // Get the current state from cloudscale.ch
410431 return actualLbState (ctx , & l .lbs , serviceInfo )
@@ -442,6 +463,28 @@ func (l *loadbalancer) EnsureLoadBalancer(
442463 return result , nil
443464}
444465
466+ func filterNodesBySelector (
467+ serviceInfo * serviceInfo ,
468+ nodes []* v1.Node ,
469+ ) ([]* v1.Node , error ) {
470+ selector := labels .Everything ()
471+ if v := serviceInfo .annotation (LoadBalancerNodeSelector ); v != "" {
472+ var err error
473+ selector , err = labels .Parse (v )
474+ if err != nil {
475+ return nil , fmt .Errorf ("unable to parse selector: %w" , err )
476+ }
477+ }
478+ selectedNodes := make ([]* v1.Node , 0 , len (nodes ))
479+ for _ , node := range nodes {
480+ if selector .Matches (labels .Set (node .Labels )) {
481+ selectedNodes = append (selectedNodes , node )
482+ }
483+ }
484+
485+ return selectedNodes , nil
486+ }
487+
445488// UpdateLoadBalancer updates hosts under the specified load balancer.
446489// Implementations must treat the *v1.Service and *v1.Node
447490// parameters as read-only and not modify them.
@@ -461,16 +504,34 @@ func (l *loadbalancer) UpdateLoadBalancer(
461504 return err
462505 }
463506
507+ filteredNodes , err := filterNodesBySelector (serviceInfo , nodes )
508+ if err != nil {
509+ return err
510+ }
511+
512+ if len (filteredNodes ) == 0 {
513+ l .recorder .Event (
514+ service ,
515+ v1 .EventTypeWarning ,
516+ "NoValidNodes" ,
517+ fmt .Sprintf ("No valid nodes for service found, " +
518+ "double-check node-selector annotation: %s: %s" ,
519+ LoadBalancerNodeSelector ,
520+ serviceInfo .annotation (LoadBalancerNodeSelector ),
521+ ),
522+ )
523+ }
524+
464525 // Reconcile
465526 return reconcileLbState (ctx , l .lbs .client , func () (* lbState , error ) {
466527 // Get the desired state from Kubernetes
467- servers , err := l .srv .mapNodes (ctx , nodes ).All ()
528+ servers , err := l .srv .mapNodes (ctx , filteredNodes ).All ()
468529 if err != nil {
469530 return nil , fmt .Errorf (
470531 "unable to get load balancer for %s: %w" , service .Name , err )
471532 }
472533
473- return desiredLbState (serviceInfo , nodes , servers )
534+ return desiredLbState (serviceInfo , filteredNodes , servers )
474535 }, func () (* lbState , error ) {
475536 // Get the current state from cloudscale.ch
476537 return actualLbState (ctx , & l .lbs , serviceInfo )
0 commit comments