github.com/cilium/cilium@v1.16.2/operator/pkg/nodeipam/nodesvclb.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package nodeipam
     5  
     6  import (
     7  	"context"
     8  	"slices"
     9  	"sort"
    10  
    11  	"github.com/sirupsen/logrus"
    12  	corev1 "k8s.io/api/core/v1"
    13  	discoveryv1 "k8s.io/api/discovery/v1"
    14  	k8serrors "k8s.io/apimachinery/pkg/api/errors"
    15  	"k8s.io/apimachinery/pkg/labels"
    16  	"k8s.io/apimachinery/pkg/runtime"
    17  	"k8s.io/apimachinery/pkg/selection"
    18  	"k8s.io/apimachinery/pkg/util/sets"
    19  	utilsnet "k8s.io/utils/net"
    20  	ctrl "sigs.k8s.io/controller-runtime"
    21  	"sigs.k8s.io/controller-runtime/pkg/builder"
    22  	"sigs.k8s.io/controller-runtime/pkg/client"
    23  	"sigs.k8s.io/controller-runtime/pkg/handler"
    24  	"sigs.k8s.io/controller-runtime/pkg/predicate"
    25  	"sigs.k8s.io/controller-runtime/pkg/reconcile"
    26  
    27  	controllerruntime "github.com/cilium/cilium/operator/pkg/controller-runtime"
    28  	"github.com/cilium/cilium/pkg/annotation"
    29  	"github.com/cilium/cilium/pkg/logging/logfields"
    30  )
    31  
    32  var (
    33  	nodeSvcLBClass                 = annotation.Prefix + "/node"
    34  	nodeSvcLBMatchLabelsAnnotation = annotation.Prefix + ".nodeipam" + "/match-node-labels"
    35  )
    36  
    37  type nodeSvcLBReconciler struct {
    38  	client.Client
    39  	Scheme *runtime.Scheme
    40  	Logger logrus.FieldLogger
    41  }
    42  
    43  func newNodeSvcLBReconciler(mgr ctrl.Manager, logger logrus.FieldLogger) *nodeSvcLBReconciler {
    44  	return &nodeSvcLBReconciler{
    45  		Client: mgr.GetClient(),
    46  		Scheme: mgr.GetScheme(),
    47  		Logger: logger,
    48  	}
    49  }
    50  
    51  // SetupWithManager sets up the controller with the Manager.
    52  func (r *nodeSvcLBReconciler) SetupWithManager(mgr ctrl.Manager) error {
    53  	filterSvc := func(obj client.Object) bool {
    54  		svc, ok := obj.(*corev1.Service)
    55  		return ok && r.isServiceSupported(svc)
    56  	}
    57  
    58  	return ctrl.NewControllerManagedBy(mgr).
    59  		// Watch for changes to Services supported by the controller
    60  		For(&corev1.Service{}, builder.WithPredicates(predicate.NewPredicateFuncs(filterSvc))).
    61  		// Watch for changes to EndpointSlices
    62  		Watches(&discoveryv1.EndpointSlice{}, r.enqueueRequestForEndpointSlice()).
    63  		// Watch for changes to Nodes
    64  		Watches(&corev1.Node{}, r.enqueueRequestForNode()).
    65  		Complete(r)
    66  }
    67  
    68  // enqueueRequestForEndpointSlice enqueue the service if a corresponding Enndpoint Slice is updated
    69  func (r *nodeSvcLBReconciler) enqueueRequestForEndpointSlice() handler.EventHandler {
    70  	return handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request {
    71  		scopedLog := r.Logger.WithFields(logrus.Fields{
    72  			logfields.Controller: "node-service-lb",
    73  			logfields.Resource:   client.ObjectKeyFromObject(o),
    74  		})
    75  		epSlice, ok := o.(*discoveryv1.EndpointSlice)
    76  		if !ok {
    77  			return []ctrl.Request{}
    78  		}
    79  		svcName, ok := epSlice.Labels[discoveryv1.LabelServiceName]
    80  		if !ok {
    81  			return []ctrl.Request{}
    82  		}
    83  		svc := client.ObjectKey{
    84  			Namespace: epSlice.GetNamespace(),
    85  			Name:      svcName,
    86  		}
    87  		scopedLog.WithField("service", svc).Info("Enqueued Service")
    88  		return []ctrl.Request{{NamespacedName: svc}}
    89  	})
    90  }
    91  
    92  // enqueueRequestForNode enqueues all Nodes that are candidates to be included
    93  // by nodeipam (see shouldIncludeNode)
    94  func (r *nodeSvcLBReconciler) enqueueRequestForNode() handler.EventHandler {
    95  	return handler.EnqueueRequestsFromMapFunc(func(ctx context.Context, o client.Object) []reconcile.Request {
    96  		scopedLog := r.Logger.WithFields(logrus.Fields{
    97  			logfields.Controller: "node-service-lb",
    98  			logfields.Resource:   client.ObjectKeyFromObject(o),
    99  		})
   100  
   101  		node, ok := o.(*corev1.Node)
   102  		if !ok {
   103  			return []ctrl.Request{}
   104  		}
   105  		if !shouldIncludeNode(node) {
   106  			return []ctrl.Request{}
   107  		}
   108  
   109  		svcList := &corev1.ServiceList{}
   110  		if err := r.Client.List(ctx, svcList, &client.ListOptions{}); err != nil {
   111  			scopedLog.WithError(err).Error("Failed to get Services")
   112  			return []reconcile.Request{}
   113  		}
   114  		requests := []reconcile.Request{}
   115  		for _, item := range svcList.Items {
   116  			if !r.isServiceSupported(&item) {
   117  				continue
   118  			}
   119  			svc := client.ObjectKey{
   120  				Namespace: item.GetNamespace(),
   121  				Name:      item.GetName(),
   122  			}
   123  			requests = append(requests, reconcile.Request{NamespacedName: svc})
   124  			scopedLog.WithField("service", svc).Info("Enqueued Service")
   125  		}
   126  		return requests
   127  	})
   128  }
   129  
   130  func (r *nodeSvcLBReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
   131  	scopedLog := r.Logger.WithFields(logrus.Fields{
   132  		logfields.Controller: "node-service-lb",
   133  		logfields.Resource:   req.NamespacedName,
   134  	})
   135  	scopedLog.Info("Reconciling Service")
   136  
   137  	svc := corev1.Service{}
   138  	err := r.Get(ctx, req.NamespacedName, &svc)
   139  	if err != nil {
   140  		if k8serrors.IsNotFound(err) {
   141  			return controllerruntime.Success()
   142  		}
   143  		return controllerruntime.Fail(err)
   144  	}
   145  
   146  	if !r.isServiceSupported(&svc) {
   147  		return controllerruntime.Success()
   148  	}
   149  
   150  	nodes, err := r.getRelevantNodes(ctx, &svc)
   151  	if err != nil {
   152  		return controllerruntime.Fail(err)
   153  	}
   154  	svc.Status.LoadBalancer.Ingress = getNodeLoadBalancerIngresses(nodes, svc.Spec.IPFamilies)
   155  	return controllerruntime.Fail(r.Status().Update(ctx, &svc))
   156  }
   157  
   158  // isServiceSupported returns true if the service is supported by the controller
   159  func (r nodeSvcLBReconciler) isServiceSupported(svc *corev1.Service) bool {
   160  	if !svc.DeletionTimestamp.IsZero() {
   161  		return false
   162  	}
   163  	if svc.Spec.Type != corev1.ServiceTypeLoadBalancer {
   164  		return false
   165  	}
   166  	return svc.Spec.LoadBalancerClass != nil &&
   167  		*svc.Spec.LoadBalancerClass == nodeSvcLBClass
   168  }
   169  
   170  // getEndpointSliceNodes returns the set of node names if eTP=Local. If eTP=Cluster
   171  // the set returned will be nil.
   172  func (r *nodeSvcLBReconciler) getEndpointSliceNodeNames(ctx context.Context, svc *corev1.Service) (sets.Set[string], error) {
   173  	// If eTP=Cluster we don't filter nodes on EndpointSlice
   174  	if svc.Spec.ExternalTrafficPolicy != corev1.ServiceExternalTrafficPolicyLocal {
   175  		return nil, nil
   176  	}
   177  
   178  	selectedNodes := sets.Set[string]{}
   179  	serviceReq, _ := labels.NewRequirement(discoveryv1.LabelServiceName, selection.Equals, []string{svc.Name})
   180  
   181  	selector := labels.NewSelector()
   182  	selector = selector.Add(*serviceReq)
   183  
   184  	var epSliceList discoveryv1.EndpointSliceList
   185  	if err := r.List(ctx, &epSliceList, &client.ListOptions{Namespace: svc.Namespace, LabelSelector: selector}); err != nil && !k8serrors.IsNotFound(err) {
   186  		return nil, err
   187  	}
   188  
   189  	for _, item := range epSliceList.Items {
   190  		for _, endpoint := range item.Endpoints {
   191  			if endpoint.Conditions.Ready != nil && !*endpoint.Conditions.Ready {
   192  				continue
   193  			}
   194  			if endpoint.NodeName == nil {
   195  				continue
   196  			}
   197  
   198  			selectedNodes.Insert(*endpoint.NodeName)
   199  		}
   200  	}
   201  
   202  	return selectedNodes, nil
   203  }
   204  
   205  // getRelevantNodes gets all the nodes candidates for seletion by nodeipam
   206  func (r *nodeSvcLBReconciler) getRelevantNodes(ctx context.Context, svc *corev1.Service) ([]corev1.Node, error) {
   207  	scopedLog := r.Logger.WithFields(logrus.Fields{
   208  		logfields.Controller: "node-service-lb",
   209  		logfields.Resource:   client.ObjectKeyFromObject(svc),
   210  	})
   211  
   212  	endpointSliceNames, err := r.getEndpointSliceNodeNames(ctx, svc)
   213  	if err != nil {
   214  		return []corev1.Node{}, err
   215  	}
   216  	nodeListOptions := &client.ListOptions{}
   217  	if val, ok := svc.Annotations[nodeSvcLBMatchLabelsAnnotation]; ok {
   218  		parsedLabels, err := labels.Parse(val)
   219  		if err != nil {
   220  			return []corev1.Node{}, err
   221  		}
   222  		nodeListOptions.LabelSelector = parsedLabels
   223  	}
   224  
   225  	var nodes corev1.NodeList
   226  	if err := r.List(ctx, &nodes, nodeListOptions); err != nil {
   227  		return []corev1.Node{}, err
   228  	}
   229  	if len(nodes.Items) == 0 {
   230  		scopedLog.WithFields(logrus.Fields{logfields.Labels: nodeListOptions.LabelSelector}).Warning("No Nodes found with configured label selector")
   231  	}
   232  
   233  	relevantNodes := []corev1.Node{}
   234  	for _, node := range nodes.Items {
   235  		if !shouldIncludeNode(&node) {
   236  			continue
   237  		}
   238  		if endpointSliceNames != nil && !endpointSliceNames.Has(node.Name) {
   239  			continue
   240  		}
   241  
   242  		relevantNodes = append(relevantNodes, node)
   243  	}
   244  	return relevantNodes, nil
   245  }
   246  
   247  // getNodeLoadBalancerIngresses get all the load balancer ingresses with the specified nodes
   248  // and IPFamilies. It will prioritize external IPs of the nodes if it can find some
   249  // or internal IPs otherwise.
   250  func getNodeLoadBalancerIngresses(nodes []corev1.Node, ipFamilies []corev1.IPFamily) []corev1.LoadBalancerIngress {
   251  	hasV4 := slices.Contains(ipFamilies, corev1.IPv4Protocol)
   252  	hasV6 := slices.Contains(ipFamilies, corev1.IPv6Protocol)
   253  	extIPs := sets.Set[string]{}
   254  	intIPs := sets.Set[string]{}
   255  	for _, node := range nodes {
   256  		for _, addr := range node.Status.Addresses {
   257  			var currentIps *sets.Set[string]
   258  			switch addr.Type {
   259  			case corev1.NodeExternalIP:
   260  				currentIps = &extIPs
   261  			case corev1.NodeInternalIP:
   262  				currentIps = &intIPs
   263  			default:
   264  				continue
   265  			}
   266  
   267  			switch {
   268  			case hasV4 && utilsnet.IsIPv4String(addr.Address):
   269  				currentIps.Insert(addr.Address)
   270  			case hasV6 && utilsnet.IsIPv6String(addr.Address):
   271  				currentIps.Insert(addr.Address)
   272  			}
   273  		}
   274  	}
   275  
   276  	var ips []string
   277  	if extIPs.Len() > 0 {
   278  		ips = extIPs.UnsortedList()
   279  	} else {
   280  		ips = intIPs.UnsortedList()
   281  	}
   282  	sort.Strings(ips)
   283  
   284  	ingresses := make([]corev1.LoadBalancerIngress, len(ips))
   285  	for i, ip := range ips {
   286  		ingresses[i] = corev1.LoadBalancerIngress{IP: ip}
   287  	}
   288  
   289  	return ingresses
   290  }