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 }