@@ -2,7 +2,6 @@ package cloudscale_ccm
22
33import (
44 "context"
5- "errors"
65 "fmt"
76 "slices"
87 "strings"
@@ -11,7 +10,9 @@ import (
1110 "github.com/cloudscale-ch/cloudscale-go-sdk/v6"
1211 v1 "k8s.io/api/core/v1"
1312 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+ "k8s.io/apimachinery/pkg/labels"
1414 "k8s.io/client-go/kubernetes"
15+ "k8s.io/client-go/tools/record"
1516 "k8s.io/klog/v2"
1617 "k8s.io/utils/ptr"
1718)
@@ -208,7 +209,7 @@ const (
208209 // connections timing out while the monitor is updated.
209210 LoadBalancerHealthMonitorTimeoutS = "k8s.cloudscale.ch/loadbalancer-health-monitor-timeout-s"
210211
211- // LoadBalancerHealthMonitorDownThreshold is the number of the checks that
212+ // LoadBalancerHealthMonitorUpThreshold is the number of the checks that
212213 // need to succeed before a pool member is considered up. Defaults to 2.
213214 LoadBalancerHealthMonitorUpThreshold = "k8s.cloudscale.ch/loadbalancer-health-monitor-up-threshold"
214215
@@ -278,7 +279,7 @@ const (
278279 // Changing this annotation on an established service is considered safe.
279280 LoadBalancerListenerTimeoutMemberDataMS = "k8s.cloudscale.ch/loadbalancer-timeout-member-data-ms"
280281
281- // LoadBalancerSubnetLimit is a JSON list of subnet UUIDs that the
282+ // LoadBalancerListenerAllowedSubnets is a JSON list of subnet UUIDs that the
282283 // loadbalancer should use. By default, all subnets of a node are used:
283284 //
284285 // * `[]` means that anyone is allowed to connect (default).
@@ -291,12 +292,17 @@ const (
291292 // This is an advanced feature, useful if you have nodes that are in
292293 // multiple private subnets.
293294 LoadBalancerListenerAllowedSubnets = "k8s.cloudscale.ch/loadbalancer-listener-allowed-subnets"
295+
296+ // LoadBalancerNodeSelector can be set to restrict which nodes are added to the LB pool.
297+ // It accepts a standard Kubernetes label selector string.
298+ LoadBalancerNodeSelector = "k8s.cloudscale.ch/loadbalancer-node-selector"
294299)
295300
296301type loadbalancer struct {
297- lbs lbMapper
298- srv serverMapper
299- k8s kubernetes.Interface
302+ lbs lbMapper
303+ srv serverMapper
304+ k8s kubernetes.Interface
305+ recorder record.EventRecorder
300306}
301307
302308// GetLoadBalancer returns whether the specified load balancer exists, and
@@ -387,16 +393,23 @@ func (l *loadbalancer) EnsureLoadBalancer(
387393 return nil , err
388394 }
389395
390- // Refuse to do anything if there are no nodes
396+ nodes , err := filterNodesBySelector (serviceInfo , nodes )
397+ if err != nil {
398+ return nil , err
399+ }
400+
391401 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" ,
402+ l .recorder .Event (
403+ service ,
404+ v1 .EventTypeWarning ,
405+ "NoValidNodes" ,
406+ "No valid nodes for service found, " +
407+ "double-check node-selector annotation" ,
395408 )
396409 }
397410
398411 // Reconcile
399- err : = reconcileLbState (ctx , l .lbs .client , func () (* lbState , error ) {
412+ err = reconcileLbState (ctx , l .lbs .client , func () (* lbState , error ) {
400413 // Get the desired state from Kubernetes
401414 servers , err := l .srv .mapNodes (ctx , nodes ).All ()
402415 if err != nil {
@@ -442,6 +455,28 @@ func (l *loadbalancer) EnsureLoadBalancer(
442455 return result , nil
443456}
444457
458+ func filterNodesBySelector (
459+ serviceInfo * serviceInfo ,
460+ nodes []* v1.Node ,
461+ ) ([]* v1.Node , error ) {
462+ selector := labels .Everything ()
463+ if v := serviceInfo .annotation (LoadBalancerNodeSelector ); v != "" {
464+ var err error
465+ selector , err = labels .Parse (v )
466+ if err != nil {
467+ return nil , fmt .Errorf ("unable to parse selector: %w" , err )
468+ }
469+ }
470+ selectedNodes := make ([]* v1.Node , 0 , len (nodes ))
471+ for _ , node := range nodes {
472+ if selector .Matches (labels .Set (node .Labels )) {
473+ selectedNodes = append (selectedNodes , node )
474+ }
475+ }
476+
477+ return selectedNodes , nil
478+ }
479+
445480// UpdateLoadBalancer updates hosts under the specified load balancer.
446481// Implementations must treat the *v1.Service and *v1.Node
447482// parameters as read-only and not modify them.
@@ -461,6 +496,21 @@ func (l *loadbalancer) UpdateLoadBalancer(
461496 return err
462497 }
463498
499+ nodes , err := filterNodesBySelector (serviceInfo , nodes )
500+ if err != nil {
501+ return err
502+ }
503+
504+ if len (nodes ) == 0 {
505+ l .recorder .Event (
506+ service ,
507+ v1 .EventTypeWarning ,
508+ "NoValidNodes" ,
509+ "No valid nodes for service found, " +
510+ "double-check node-selector annotation" ,
511+ )
512+ }
513+
464514 // Reconcile
465515 return reconcileLbState (ctx , l .lbs .client , func () (* lbState , error ) {
466516 // Get the desired state from Kubernetes
0 commit comments