• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kubeovn / kube-ovn / 29910193899

22 Jul 2026 09:59AM UTC coverage: 27.884% (+0.07%) from 27.817%
29910193899

Pull #7040

github

changluyi
fix(metallb): preserve internal underlay VIP traffic

Signed-off-by: clyi <clyi@alauda.io>
Pull Request #7040: feature(metallb): internal underlay pod traffic to LB service VIP node

112 of 258 new or added lines in 5 files covered. (43.41%)

17057 of 61172 relevant lines covered (27.88%)

0.32 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

21.85
/pkg/controller/endpoint_slice.go
1
package controller
2

3
import (
4
        "context"
5
        "fmt"
6
        "reflect"
7
        "slices"
8
        "strings"
9
        "time"
10

11
        v1 "k8s.io/api/core/v1"
12
        discoveryv1 "k8s.io/api/discovery/v1"
13
        "k8s.io/apimachinery/pkg/api/errors"
14
        metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15
        "k8s.io/apimachinery/pkg/labels"
16
        utilruntime "k8s.io/apimachinery/pkg/util/runtime"
17
        "k8s.io/client-go/tools/cache"
18
        "k8s.io/klog/v2"
19

20
        kubeovnv1 "github.com/kubeovn/kube-ovn/pkg/apis/kubeovn/v1"
21
        "github.com/kubeovn/kube-ovn/pkg/ovs"
22
        "github.com/kubeovn/kube-ovn/pkg/util"
23
)
24

25
type IPPortMapping map[string]string
26

27
// getServiceForEndpointSlice returns the service linked to an EndpointSlice
28
func getServiceForEndpointSlice(endpointSlice *discoveryv1.EndpointSlice) string {
1✔
29
        if endpointSlice != nil && endpointSlice.Labels != nil {
2✔
30
                return endpointSlice.Labels[discoveryv1.LabelServiceName]
1✔
31
        }
1✔
32

33
        return ""
1✔
34
}
35

36
func findServiceKey(endpointSlice *discoveryv1.EndpointSlice) string {
1✔
37
        service := getServiceForEndpointSlice(endpointSlice)
1✔
38
        if service == "" {
2✔
39
                return ""
1✔
40
        }
1✔
41

42
        return endpointSlice.Namespace + "/" + service
1✔
43
}
44

45
func (c *Controller) enqueueAddEndpointSlice(obj any) {
1✔
46
        if !c.config.EnableLb {
2✔
47
                return
1✔
48
        }
1✔
49

50
        key := findServiceKey(obj.(*discoveryv1.EndpointSlice))
1✔
51
        if key != "" {
2✔
52
                klog.V(3).Infof("enqueue add endpointSlice %s", key)
1✔
53
                c.addOrUpdateEndpointSliceQueue.Add(key)
1✔
54
        }
1✔
55
}
56

57
func (c *Controller) enqueueUpdateEndpointSlice(oldObj, newObj any) {
1✔
58
        if !c.config.EnableLb {
2✔
59
                return
1✔
60
        }
1✔
61

62
        oldEndpointSlice := oldObj.(*discoveryv1.EndpointSlice)
1✔
63
        newEndpointSlice := newObj.(*discoveryv1.EndpointSlice)
1✔
64
        if oldEndpointSlice.ResourceVersion == newEndpointSlice.ResourceVersion {
1✔
65
                return
×
66
        }
×
67

68
        if len(oldEndpointSlice.Endpoints) == 0 && len(newEndpointSlice.Endpoints) == 0 {
1✔
69
                return
×
70
        }
×
71

72
        // skip metadata-only churn, e.g. the endpoints.kubernetes.io/last-change-trigger-time
73
        // annotation refresh: the LB backends are computed solely from endpoints, ports and
74
        // the owning service.
75
        if getServiceForEndpointSlice(oldEndpointSlice) == getServiceForEndpointSlice(newEndpointSlice) &&
1✔
76
                reflect.DeepEqual(oldEndpointSlice.Endpoints, newEndpointSlice.Endpoints) &&
1✔
77
                reflect.DeepEqual(oldEndpointSlice.Ports, newEndpointSlice.Ports) {
2✔
78
                return
1✔
79
        }
1✔
80

81
        key := findServiceKey(newEndpointSlice)
1✔
82
        if key != "" {
2✔
83
                klog.V(3).Infof("enqueue update endpointSlice for service %s", key)
1✔
84
                c.addOrUpdateEndpointSliceQueue.Add(key)
1✔
85
        }
1✔
86
}
87

88
func (c *Controller) handleUpdateEndpointSlice(key string) error {
×
89
        namespace, name, err := cache.SplitMetaNamespaceKey(key)
×
90
        if err != nil {
×
91
                utilruntime.HandleError(fmt.Errorf("invalid resource key: %s", key))
×
92
                return nil
×
93
        }
×
94

95
        c.epKeyMutex.LockKey(key)
×
96
        defer func() { _ = c.epKeyMutex.UnlockKey(key) }()
×
97
        klog.Infof("handle update endpointSlice for service %s", key)
×
98

×
99
        endpointSlices, err := c.endpointSlicesLister.EndpointSlices(namespace).List(labels.Set{discoveryv1.LabelServiceName: name}.AsSelector())
×
100
        if err != nil {
×
101
                if errors.IsNotFound(err) {
×
102
                        return nil
×
103
                }
×
104
                klog.Error(err)
×
105
                return err
×
106
        }
107

108
        cachedService, err := c.servicesLister.Services(namespace).Get(name)
×
109
        if err != nil {
×
110
                if errors.IsNotFound(err) {
×
111
                        return nil
×
112
                }
×
113
                klog.Error(err)
×
114
                return err
×
115
        }
116
        svc := cachedService.DeepCopy()
×
117

×
118
        var (
×
119
                lbVips                   []string
×
120
                vip, vpcName, subnetName string
×
NEW
121
                externalVIPNode          string
×
NEW
122
                serviceL2StatusReady     = true
×
123
                ok                       bool
×
124
                ignoreHealthCheck        = true
×
125
                isPreferLocalBackend     = false
×
126
        )
×
127

×
128
        if vip, ok = svc.Annotations[util.SwitchLBRuleVipsAnnotation]; ok && vip != "" {
×
129
                lbVips = []string{vip}
×
130

×
131
                // Health checks can only run against IPv4 endpoints and if the service doesn't specify they must be disabled
×
132
                if util.CheckProtocol(vip) == kubeovnv1.ProtocolIPv4 && !serviceHealthChecksDisabled(svc) {
×
133
                        ignoreHealthCheck = false
×
134
                }
×
135
        } else if vip, ok = svc.Annotations[util.RouterLBRuleVipsAnnotation]; ok && vip != "" {
×
136
                for ip := range strings.SplitSeq(vip, ",") {
×
137
                        ip = strings.TrimSpace(ip)
×
138
                        if ip == "" {
×
139
                                continue
×
140
                        }
141
                        lbVips = append(lbVips, ip)
×
142
                        if util.CheckProtocol(ip) == kubeovnv1.ProtocolIPv4 && !serviceHealthChecksDisabled(svc) {
×
143
                                ignoreHealthCheck = false
×
144
                        }
×
145
                }
146
        } else if lbVips = util.ServiceClusterIPs(*svc); len(lbVips) == 0 {
×
147
                return nil
×
148
        }
×
149

150
        if c.config.EnableLb && c.config.EnableOVNLBPreferLocal {
×
151
                if svc.Spec.Type == v1.ServiceTypeLoadBalancer && svc.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeLocal {
×
152
                        if len(svc.Status.LoadBalancer.Ingress) > 0 {
×
153
                                for _, ingress := range svc.Status.LoadBalancer.Ingress {
×
154
                                        if ingress.IP != "" {
×
155
                                                lbVips = append(lbVips, ingress.IP)
×
156
                                        }
×
157
                                }
158
                        }
159
                        isPreferLocalBackend = true
×
NEW
160
                        externalVIPNode, serviceL2StatusReady, err = c.getServiceL2StatusNode(namespace, name)
×
NEW
161
                        if err != nil {
×
NEW
162
                                return err
×
NEW
163
                        }
×
164
                } else if svc.Spec.Type == v1.ServiceTypeClusterIP && svc.Spec.InternalTrafficPolicy != nil && *svc.Spec.InternalTrafficPolicy == v1.ServiceInternalTrafficPolicyLocal {
×
165
                        isPreferLocalBackend = true
×
166
                }
×
167
        }
168

169
        // If Kube-OVN is running in secondary CNI mode, the endpoint IPs should be derived from the network attachment definitions
170
        // This overwrite can be removed if endpoint construction accounts for network attachment IP address
171
        // TODO: Identify how endpoints are constructed, by default, endpoints has IP address of eth0 interface
172
        if c.config.EnableNonPrimaryCNI && serviceHasSelector(svc) {
×
173
                var pods []*v1.Pod
×
174
                if pods, err = c.podsLister.Pods(namespace).List(labels.Set(svc.Spec.Selector).AsSelector()); err != nil {
×
175
                        klog.Errorf("failed to get pods for service %s in namespace %s: %v", name, namespace, err)
×
176
                        return err
×
177
                }
×
178
                err = c.replaceEndpointAddressesWithSecondaryIPs(endpointSlices, pods)
×
179
                if err != nil {
×
180
                        klog.Errorf("failed to update endpointSlice: %v", err)
×
181
                        return err
×
182
                }
×
183
        }
184

185
        vpcName, subnetName, err = c.getVpcAndSubnetForEndpoints(endpointSlices, svc)
×
186
        if err != nil {
×
187
                return err
×
188
        }
×
189

190
        var (
×
191
                vpc    *kubeovnv1.Vpc
×
192
                svcVpc string
×
193
        )
×
194

×
195
        if vpc, err = c.vpcsLister.Get(vpcName); err != nil {
×
196
                klog.Errorf("failed to get vpc %s, %v", vpcName, err)
×
197
                return err
×
198
        }
×
199

200
        tcpLb, udpLb, sctpLb := vpc.Status.TCPLoadBalancer, vpc.Status.UDPLoadBalancer, vpc.Status.SctpLoadBalancer
×
201
        oldTCPLb, oldUDPLb, oldSctpLb := vpc.Status.TCPSessionLoadBalancer, vpc.Status.UDPSessionLoadBalancer, vpc.Status.SctpSessionLoadBalancer
×
202
        if svc.Spec.SessionAffinity == v1.ServiceAffinityClientIP {
×
203
                tcpLb, udpLb, sctpLb, oldTCPLb, oldUDPLb, oldSctpLb = oldTCPLb, oldUDPLb, oldSctpLb, tcpLb, udpLb, sctpLb
×
204
        }
×
NEW
205
        if c.config.EnableOVNLBPreferLocal {
×
NEW
206
                if err = c.clearLoadBalancerVIPExternalTrafficLocal(svc, tcpLb, udpLb, sctpLb); err != nil {
×
NEW
207
                        return err
×
NEW
208
                }
×
209
        }
210

211
        for _, lbVip := range lbVips {
×
212
                for _, port := range svc.Spec.Ports {
×
213
                        var lb, oldLb string
×
214
                        switch port.Protocol {
×
215
                        case v1.ProtocolTCP:
×
216
                                lb, oldLb = tcpLb, oldTCPLb
×
217
                        case v1.ProtocolUDP:
×
218
                                lb, oldLb = udpLb, oldUDPLb
×
219
                        case v1.ProtocolSCTP:
×
220
                                lb, oldLb = sctpLb, oldSctpLb
×
221
                        }
222

223
                        var (
×
224
                                vip, checkIP             string
×
225
                                backends                 []string
×
226
                                ipPortMapping, externals map[string]string
×
227
                        )
×
228

×
229
                        if !ignoreHealthCheck {
×
230
                                if checkIP, err = c.getHealthCheckVip(subnetName, lbVip); err != nil {
×
231
                                        klog.Error(err)
×
232
                                        return err
×
233
                                }
×
234
                                externals = map[string]string{
×
235
                                        util.SwitchLBRuleSubnet: subnetName,
×
236
                                }
×
237
                        }
238

239
                        if isPreferLocalBackend {
×
240
                                // only use the ipportmapping's lsp to ip map when the backend is local
×
241
                                checkIP = util.MasqueradeCheckIP
×
242
                        }
×
243

244
                        backends = c.getEndpointBackend(endpointSlices, port, lbVip)
×
245

×
246
                        if !ignoreHealthCheck || isPreferLocalBackend {
×
247
                                ipPortMapping, err = c.getIPPortMapping(endpointSlices, svc, checkIP)
×
248
                                if err != nil {
×
249
                                        err := fmt.Errorf("couldn't get ip port mapping for svc %s/%s: %w", svc.Namespace, svc.Name, err)
×
250
                                        return err
×
251
                                }
×
252
                        }
253

254
                        // for performance reason delete lb with no backends
255
                        if len(backends) != 0 {
×
256
                                vip = util.JoinHostPort(lbVip, port.Port)
×
257
                                klog.Infof("add vip endpoint %s, backends %v to LB %s", vip, backends, lb)
×
258
                                if err = c.OVNNbClient.LoadBalancerAddVip(lb, vip, backends...); err != nil {
×
259
                                        klog.Errorf("failed to add vip %s with backends %s to LB %s: %v", lbVip, backends, lb, err)
×
260
                                        return err
×
261
                                }
×
NEW
262
                                if isPreferLocalBackend &&
×
NEW
263
                                        svc.Spec.Type == v1.ServiceTypeLoadBalancer &&
×
NEW
264
                                        svc.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeLocal &&
×
NEW
265
                                        serviceL2StatusReady &&
×
NEW
266
                                        slices.ContainsFunc(svc.Status.LoadBalancer.Ingress, func(ingress v1.LoadBalancerIngress) bool {
×
NEW
267
                                                return ingress.IP == lbVip
×
NEW
268
                                        }) {
×
NEW
269
                                        vipNodeLSP := ""
×
NEW
270
                                        if externalVIPNode != "" {
×
NEW
271
                                                vipNodeLSP = util.NodeLspName(externalVIPNode)
×
NEW
272
                                        }
×
NEW
273
                                        if err = c.OVNNbClient.SetLoadBalancerVIPExternalTrafficLocal(lb, vip, vipNodeLSP); err != nil {
×
NEW
274
                                                return fmt.Errorf("couldn't mark external local vip %s on LB %s: %w", vip, lb, err)
×
NEW
275
                                        }
×
276
                                }
277

278
                                if isPreferLocalBackend && len(ipPortMapping) != 0 {
×
279
                                        if err = c.OVNNbClient.LoadBalancerUpdateIPPortMapping(lb, vip, ipPortMapping); err != nil {
×
280
                                                klog.Errorf("failed to update ip port mapping %s for vip %s to LB %s: %v", ipPortMapping, vip, lb, err)
×
281
                                                return err
×
282
                                        }
×
283
                                }
284

285
                                if !ignoreHealthCheck {
×
286
                                        klog.Infof("add health check ip port mapping %v to LB %s", ipPortMapping, lb)
×
287
                                        if err = c.OVNNbClient.LoadBalancerAddHealthCheck(lb, vip, ignoreHealthCheck, ipPortMapping, externals); err != nil {
×
288
                                                klog.Errorf("failed to add health check for vip %s with ip port mapping %s to LB %s: %v", lbVip, ipPortMapping, lb, err)
×
289
                                                return err
×
290
                                        }
×
291
                                }
292
                        } else {
×
293
                                vip = util.JoinHostPort(lbVip, port.Port)
×
294
                                klog.V(3).Infof("delete vip endpoint %s from LB %s", vip, lb)
×
295
                                if err = c.OVNNbClient.LoadBalancerDeleteVip(lb, vip, true); err != nil {
×
296
                                        klog.Errorf("failed to delete vip endpoint %s from LB %s: %v", vip, lb, err)
×
297
                                        return err
×
298
                                }
×
299

300
                                klog.V(3).Infof("delete vip endpoint %s from old LB %s", vip, oldLb)
×
301
                                if err = c.OVNNbClient.LoadBalancerDeleteVip(oldLb, vip, true); err != nil {
×
302
                                        klog.Errorf("failed to delete vip %s from LB %s: %v", vip, oldLb, err)
×
303
                                        return err
×
304
                                }
×
305

306
                                if c.config.EnableOVNLBPreferLocal {
×
307
                                        if err := c.OVNNbClient.LoadBalancerDeleteIPPortMapping(lb, vip); err != nil {
×
308
                                                klog.Errorf("failed to delete ip port mapping for vip %s from LB %s: %v", vip, lb, err)
×
309
                                                return err
×
310
                                        }
×
311
                                        if err := c.OVNNbClient.LoadBalancerDeleteIPPortMapping(oldLb, vip); err != nil {
×
312
                                                klog.Errorf("failed to delete ip port mapping for vip %s from LB %s: %v", vip, lb, err)
×
313
                                                return err
×
314
                                        }
×
315
                                }
316
                        }
317
                }
318
        }
319

320
        if svcVpc = svc.Annotations[util.VpcAnnotation]; svcVpc != vpcName {
×
321
                patch := util.KVPatch{util.VpcAnnotation: vpcName}
×
322
                if err = util.PatchAnnotations(c.config.KubeClient.CoreV1().Services(namespace), svc.Name, patch); err != nil {
×
323
                        klog.Errorf("failed to patch service %s: %v", key, err)
×
324
                        return err
×
325
                }
×
326
        }
327

328
        return nil
×
329
}
330

331
// Update the endpoint IP address with the secondary IP address of the pod using the network attachment definition annotation
332
// This is a temporary fix to allow consumers to use the secondary IP address of the pod
333
// TODO: Remove this function and update the endpoint construction to use the secondary IP address of the pod
334
func (c *Controller) replaceEndpointAddressesWithSecondaryIPs(endpointSlices []*discoveryv1.EndpointSlice, pods []*v1.Pod) error {
1✔
335
        // Track which pods have been processed
1✔
336
        processedPods := make(map[string]bool)
1✔
337
        // Store pod information in a map
1✔
338
        podMap := make(map[string]*v1.Pod, len(pods))
1✔
339
        for i := range pods {
2✔
340
                podMap[pods[i].Name] = pods[i]
1✔
341
        }
1✔
342
        // Pre-compute secondary IPs for all pods to avoid repeated annotation lookups
343
        secondaryIPs := make(map[string]string, len(pods))
1✔
344
        for _, pod := range pods {
2✔
345
                providers, err := c.getPodProviders(pod)
1✔
346
                if err != nil {
1✔
347
                        return err
×
348
                }
×
349
                if len(providers) > 0 {
2✔
350
                        ipAddress := pod.Annotations[fmt.Sprintf(util.IPAddressAnnotationTemplate, providers[0])]
1✔
351
                        if ipAddress != "" {
2✔
352
                                secondaryIPs[pod.Name] = ipAddress
1✔
353
                        }
1✔
354
                }
355
        }
356
        // Process each endpoint slice
357
        for i, endpoint := range endpointSlices {
2✔
358
                var copiedSlice *discoveryv1.EndpointSlice
1✔
359
                needsUpdate := false
1✔
360
                // Check if any endpoints need updating first
1✔
361
                for j, ep := range endpoint.Endpoints {
2✔
362
                        if ep.TargetRef != nil && ep.TargetRef.Kind == util.KindPod {
2✔
363
                                podName := ep.TargetRef.Name
1✔
364
                                // Skip if already processed this pod
1✔
365
                                // Include slice index to handle pod in multiple slices
1✔
366
                                podKey := fmt.Sprintf("%s/%d", podName, i)
1✔
367
                                if processedPods[podKey] {
1✔
368
                                        continue
×
369
                                }
370
                                if secondaryIP, hasSecondaryIP := secondaryIPs[podName]; hasSecondaryIP {
2✔
371
                                        if pod, ok := podMap[podName]; ok {
2✔
372
                                                // Check if any address needs replacement
1✔
373
                                                for k, address := range ep.Addresses {
2✔
374
                                                        // Only replace if it's the primary IP
1✔
375
                                                        if address == pod.Status.PodIP {
2✔
376
                                                                // Lazy deep copy
1✔
377
                                                                if !needsUpdate {
2✔
378
                                                                        copiedSlice = endpoint.DeepCopy()
1✔
379
                                                                        needsUpdate = true
1✔
380
                                                                }
1✔
381
                                                                klog.Infof("updating pod %s/%s ip address %s to %s",
1✔
382
                                                                        pod.Namespace, pod.Name, pod.Status.PodIP, secondaryIP)
1✔
383
                                                                copiedSlice.Endpoints[j].Addresses[k] = secondaryIP
1✔
384
                                                                processedPods[podKey] = true
1✔
385
                                                                // Only one primary IP per endpoint
1✔
386
                                                                break
1✔
387
                                                        } else if address == secondaryIP {
×
388
                                                                // Already has secondary IP, mark as processed
×
389
                                                                processedPods[podKey] = true
×
390
                                                                break
×
391
                                                        }
392
                                                }
393
                                        }
394
                                }
395
                        }
396
                }
397
                // Replace the slice if we made changes
398
                if needsUpdate {
2✔
399
                        endpointSlices[i] = copiedSlice
1✔
400
                }
1✔
401
        }
402

403
        return nil
1✔
404
}
405

406
func (c *Controller) clearLoadBalancerVIPExternalTrafficLocal(svc *v1.Service, tcpLb, udpLb, sctpLb string) error {
1✔
407
        if svc.Spec.Type != v1.ServiceTypeLoadBalancer ||
1✔
408
                svc.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeLocal {
2✔
409
                return nil
1✔
410
        }
1✔
411

412
        for _, ingress := range svc.Status.LoadBalancer.Ingress {
2✔
413
                if ingress.IP == "" {
1✔
NEW
414
                        continue
×
415
                }
416
                for _, port := range svc.Spec.Ports {
2✔
417
                        var lb string
1✔
418
                        switch port.Protocol {
1✔
419
                        case v1.ProtocolTCP:
1✔
420
                                lb = tcpLb
1✔
NEW
421
                        case v1.ProtocolUDP:
×
NEW
422
                                lb = udpLb
×
NEW
423
                        case v1.ProtocolSCTP:
×
NEW
424
                                lb = sctpLb
×
425
                        }
426
                        if lb == "" {
1✔
NEW
427
                                continue
×
428
                        }
429
                        vip := util.JoinHostPort(ingress.IP, port.Port)
1✔
430
                        if err := c.OVNNbClient.SetLoadBalancerVIPExternalTrafficLocal(lb, vip, ""); err != nil {
1✔
NEW
431
                                return fmt.Errorf("couldn't clear external local vip marker %s on LB %s: %w", vip, lb, err)
×
NEW
432
                        }
×
433
                }
434
        }
435
        return nil
1✔
436
}
437

438
// enqueueStaticEndpointUpdateInNamespace enqueues updates for every statically generated EndpointSlice in a namespace.
439
// Statically generated EndpointSlices are not generated by the selectors of their parent service.
440
func (c *Controller) enqueueStaticEndpointUpdateInNamespace(namespace string) {
×
441
        // Find all the statically generated EndpointSlices in the namespace
×
442
        endpointSlices, err := c.findStaticEndpointSlicesInNamespace(namespace)
×
443
        if err != nil {
×
444
                err := fmt.Errorf("couldn't find static endpointslices in namespace %s: %w", namespace, err)
×
445
                klog.Error(err)
×
446
        }
×
447

448
        // Enqueue updates for all the EndpointSlices
449
        for _, slice := range endpointSlices {
×
450
                c.enqueueAddEndpointSlice(slice)
×
451
        }
×
452
}
453

454
// serviceHealthChecksDisabled returns whether health checks must be omitted for a particular service
455
func serviceHealthChecksDisabled(service *v1.Service) bool {
1✔
456
        // Service must not have disabled health checks
1✔
457
        if service.Annotations != nil && service.Annotations[util.ServiceHealthCheck] == "false" {
2✔
458
                return true
1✔
459
        }
1✔
460

461
        // If nothing is specified, checks are enabled by default
462
        return false
1✔
463
}
464

465
// findStaticEndpointSlicesInNamespace finds all the EndpointSlices in a namespace that are statically generated.
466
// Statically generated EndpointSlices are not generated by the selectors of their parent service.
467
func (c *Controller) findStaticEndpointSlicesInNamespace(namespace string) ([]*discoveryv1.EndpointSlice, error) {
×
468
        // Retrieve all the services in the namespace
×
469
        services, err := c.servicesLister.Services(namespace).List(labels.Everything())
×
470
        if err != nil {
×
471
                err := fmt.Errorf("couldn't list services in namespace %s: %w", namespace, err)
×
472
                klog.Error(err)
×
473
                return nil, err
×
474
        }
×
475

476
        // Only handle services that have static endpoints provided, and not selectors
477
        var filteredServices []*v1.Service
×
478
        for _, service := range services {
×
479
                if serviceHasSelector(service) {
×
480
                        continue
×
481
                }
482

483
                filteredServices = append(filteredServices, service)
×
484
        }
485

486
        // Find the EndpointSlices linked to those services
487
        endpointSlices, err := c.findEndpointSlicesForServices(namespace, filteredServices)
×
488
        if err != nil {
×
489
                return nil, err
×
490
        }
×
491

492
        return endpointSlices, nil
×
493
}
494

495
// findEndpointSlicesForServices returns all the EndpointSlices that are linked to services in the same namespace.
496
// Parameter "namespace" is the namespace in which all the services are located.
497
// Parameter "services" is a list of all the services for which we want to find the EndpointSlices.
498
func (c *Controller) findEndpointSlicesForServices(namespace string, services []*v1.Service) ([]*discoveryv1.EndpointSlice, error) {
×
499
        var endpointSlices []*discoveryv1.EndpointSlice
×
500

×
501
        // Look up EndpointSlices for each service via the byServiceName indexer.
×
502
        for _, service := range services {
×
503
                objs, err := c.epsIndexer.ByIndex(IndexEPSByService, namespace+"/"+service.Name)
×
504
                if err != nil {
×
505
                        err := fmt.Errorf("couldn't query endpointslices for service %s/%s: %w", namespace, service.Name, err)
×
506
                        klog.Error(err)
×
507
                        return nil, err
×
508
                }
×
509
                for _, obj := range objs {
×
510
                        endpointSlices = append(endpointSlices, obj.(*discoveryv1.EndpointSlice))
×
511
                }
×
512
        }
513

514
        return endpointSlices, nil
×
515
}
516

517
// serviceHasSelector returns if a service has selectors
518
func serviceHasSelector(service *v1.Service) bool {
1✔
519
        return len(service.Spec.Selector) > 0
1✔
520
}
1✔
521

522
// getCustomServiceVpcAndSubnet returns the custom VPC/Subnet defined on a service
523
func getCustomServiceVpcAndSubnet(service *v1.Service) (vpcName, subnetName string) {
×
524
        if service.Annotations != nil {
×
525
                vpcName = service.Annotations[util.LogicalRouterAnnotation]
×
526
                subnetName = service.Annotations[util.LogicalSwitchAnnotation]
×
527
        }
×
528

529
        return vpcName, subnetName
×
530
}
531

532
// getDefaultVpcAndSubnet returns the default VPC/Subnet to apply to a LoadBalancer if nothing was found
533
// during automatic discovery. If both parameters are non-empty, they are returned as is.
534
func (c *Controller) getDefaultVpcAndSubnet(service *v1.Service, vpcName, subnetName string) (string, string) {
×
535
        // Default to what's on the service or to the default VPC
×
536
        if vpcName == "" {
×
537
                if vpcName = service.Annotations[util.VpcAnnotation]; vpcName == "" {
×
538
                        vpcName = c.config.ClusterRouter
×
539
                }
×
540
        }
541

542
        // Use the default subnet if it wasn't found
543
        if subnetName == "" {
×
544
                subnetName = util.DefaultSubnet
×
545
        }
×
546

547
        return vpcName, subnetName
×
548
}
549

550
// getVpcAndSubnetForEndpoints returns the name of the VPC/Subnet for EndpointSlices
551
func (c *Controller) getVpcAndSubnetForEndpoints(endpointSlices []*discoveryv1.EndpointSlice, service *v1.Service) (vpcName, subnetName string, err error) {
×
552
        // Let the user self-determine what VPC and subnet to use if they provided annotations on the service
×
553
        // Both the VPC and Subnet must be provided
×
554
        vpcName, subnetName = getCustomServiceVpcAndSubnet(service)
×
555
        if vpcName != "" && subnetName != "" {
×
556
                return vpcName, subnetName, nil
×
557
        }
×
558

559
        // Choose the most optimized and straightforward way to retrieve the name of the VPC and subnet
560
        if serviceHasSelector(service) {
×
561
                // The service has a selector, which means that the EndpointSlices should have targets.
×
562
                // We can use those targets instead of looking at every pod in the namespace.
×
563
                vpcName, subnetName = c.findVpcAndSubnetWithTargets(endpointSlices)
×
564
        } else {
×
565
                // The service has no selectors, we must find which pods in the namespace of the service
×
566
                // are targeted by the endpoint by only looking at the IPs.
×
567
                pods, err := c.podsLister.Pods(service.Namespace).List(labels.Everything())
×
568
                if err != nil {
×
569
                        err := fmt.Errorf("failed to get pods for service %s in namespace %s: %w", service.Name, service.Namespace, err)
×
570
                        klog.Error(err)
×
571
                        return "", "", err
×
572
                }
×
573

574
                vpcName, subnetName = c.findVpcAndSubnetWithNoTargets(endpointSlices, pods)
×
575
        }
576

577
        vpcName, subnetName = c.getDefaultVpcAndSubnet(service, vpcName, subnetName)
×
578
        return vpcName, subnetName, nil
×
579
}
580

581
// findVpcAndSubnetWithTargets returns the name of the VPC and Subnet for endpoints with targets
582
func (c *Controller) findVpcAndSubnetWithTargets(endpointSlices []*discoveryv1.EndpointSlice) (vpcName, subnetName string) {
×
583
        for _, slice := range endpointSlices {
×
584
                for _, endpoint := range slice.Endpoints {
×
585
                        if endpoint.TargetRef == nil {
×
586
                                continue
×
587
                        }
588

589
                        namespace, name := endpoint.TargetRef.Namespace, endpoint.TargetRef.Name
×
590
                        if name == "" || namespace == "" {
×
591
                                continue
×
592
                        }
593

594
                        pod, err := c.podsLister.Pods(namespace).Get(name)
×
595
                        if err != nil {
×
596
                                err := fmt.Errorf("couldn't retrieve pod %s/%s: %w", namespace, name, err)
×
597
                                klog.Error(err)
×
598
                                continue
×
599
                        }
600

601
                        vpc, subnet, err := c.getEndpointVpcAndSubnet(pod, endpoint.Addresses)
×
602
                        if err != nil {
×
603
                                err := fmt.Errorf("couldn't retrieve subnet/vpc for pod %s/%s: %w", namespace, name, err)
×
604
                                klog.Error(err)
×
605
                                continue
×
606
                        }
607

608
                        if vpcName == "" {
×
609
                                vpcName = vpc
×
610
                        }
×
611

612
                        if subnetName == "" {
×
613
                                subnetName = subnet
×
614
                        }
×
615

616
                        if vpcName != "" && subnetName != "" {
×
617
                                return vpcName, subnetName
×
618
                        }
×
619
                }
620
        }
621

622
        return vpcName, subnetName
×
623
}
624

625
// findVpcAndSubnetWithNoTargets returns the name of the VPC and Subnet for endpoints with no targets
626
func (c *Controller) findVpcAndSubnetWithNoTargets(endpointSlices []*discoveryv1.EndpointSlice, pods []*v1.Pod) (vpcName, subnetName string) {
×
627
        for _, slice := range endpointSlices {
×
628
                for _, endpoint := range slice.Endpoints {
×
629
                        for _, pod := range pods {
×
630
                                vpc, subnet, err := c.getEndpointVpcAndSubnet(pod, endpoint.Addresses)
×
631
                                if err != nil {
×
632
                                        err := fmt.Errorf("couldn't retrieve subnet/vpc for pod %s/%s: %w", pod.Namespace, pod.Name, err)
×
633
                                        klog.Error(err)
×
634
                                        continue
×
635
                                }
636

637
                                if vpcName == "" {
×
638
                                        vpcName = vpc
×
639
                                }
×
640

641
                                if subnetName == "" {
×
642
                                        subnetName = subnet
×
643
                                }
×
644

645
                                if vpcName != "" && subnetName != "" {
×
646
                                        return vpcName, subnetName
×
647
                                }
×
648
                        }
649
                }
650
        }
651

652
        return vpcName, subnetName
×
653
}
654

655
// getHealthCheckVip get health check vip for load balancer, the vip name is the subnet name
656
// the vip is used to check the health of the backend pod
657
func (c *Controller) getHealthCheckVip(subnetName, lbVip string) (string, error) {
×
658
        var (
×
659
                needCreateHealthCheckVip bool
×
660
                checkVip                 *kubeovnv1.Vip
×
661
                checkIP                  string
×
662
                err                      error
×
663
        )
×
664
        vipName := subnetName
×
665
        checkVip, err = c.virtualIpsLister.Get(vipName)
×
666
        if err != nil {
×
667
                if errors.IsNotFound(err) {
×
668
                        needCreateHealthCheckVip = true
×
669
                } else {
×
670
                        klog.Errorf("failed to get health check vip %s, %v", vipName, err)
×
671
                        return "", err
×
672
                }
×
673
        }
674
        if needCreateHealthCheckVip {
×
675
                vip := &kubeovnv1.Vip{
×
676
                        ObjectMeta: metav1.ObjectMeta{
×
677
                                Name: vipName,
×
678
                        },
×
679
                        Spec: kubeovnv1.VipSpec{
×
680
                                Subnet: subnetName,
×
681
                        },
×
682
                }
×
683
                if _, err = c.config.KubeOvnClient.KubeovnV1().Vips().Create(context.Background(), vip, metav1.CreateOptions{}); err != nil {
×
684
                        klog.Errorf("failed to create health check vip %s, %v", vipName, err)
×
685
                        return "", err
×
686
                }
×
687

688
                // wait for vip created
689
                // TODO: WATCH VIP
690
                time.Sleep(1 * time.Second)
×
691
                checkVip, err = c.virtualIpsLister.Get(vipName)
×
692
                if err != nil {
×
693
                        klog.Errorf("failed to get health check vip %s, %v", vipName, err)
×
694
                        return "", err
×
695
                }
×
696
        }
697

698
        if checkVip.Status.V4ip == "" && checkVip.Status.V6ip == "" {
×
699
                err = fmt.Errorf("vip %s is not ready", vipName)
×
700
                klog.Error(err)
×
701
                return "", err
×
702
        }
×
703

704
        switch util.CheckProtocol(lbVip) {
×
705
        case kubeovnv1.ProtocolIPv4:
×
706
                checkIP = checkVip.Status.V4ip
×
707
        case kubeovnv1.ProtocolIPv6:
×
708
                checkIP = checkVip.Status.V6ip
×
709
        }
710
        if checkIP == "" {
×
711
                err = fmt.Errorf("failed to get health check vip subnet %s", vipName)
×
712
                klog.Error(err)
×
713
                return "", err
×
714
        }
×
715

716
        return checkIP, nil
×
717
}
718

719
// getEndpointBackend returns the LB backend for a service
720
func (c *Controller) getEndpointBackend(endpointSlices []*discoveryv1.EndpointSlice, servicePort v1.ServicePort, serviceIP string) (backends []string) {
×
721
        protocol := util.CheckProtocol(serviceIP)
×
722

×
723
        for _, endpointSlice := range endpointSlices {
×
724
                var targetPort int32
×
725
                for _, port := range endpointSlice.Ports {
×
726
                        if port.Name != nil && *port.Name == servicePort.Name {
×
727
                                targetPort = *port.Port
×
728
                                break
×
729
                        }
730
                }
731
                if targetPort == 0 {
×
732
                        continue
×
733
                }
734

735
                for _, endpoint := range endpointSlice.Endpoints {
×
736
                        if !endpointReady(endpoint) {
×
737
                                continue
×
738
                        }
739

740
                        for _, address := range endpoint.Addresses {
×
741
                                if util.CheckProtocol(address) == protocol {
×
742
                                        backends = append(backends, util.JoinHostPort(address, targetPort))
×
743
                                }
×
744
                        }
745
                }
746
        }
747

748
        return backends
×
749
}
750

751
// endpointReady returns whether an endpoint can receive traffic
752
func endpointReady(endpoint discoveryv1.Endpoint) bool {
1✔
753
        return endpoint.Conditions.Ready == nil || *endpoint.Conditions.Ready
1✔
754
}
1✔
755

756
// addIPPortMappingEntry adds a new entry to an IPPortMapping for a given target, the addresses on that target and the
757
// VIP used to run the health checks
758
func (c *Controller) addIPPortMappingEntry(pod *v1.Pod, addresses []string, checkVip string, mapping IPPortMapping) error {
×
759
        // Abort if the pod is getting deleted
×
760
        if !pod.DeletionTimestamp.IsZero() {
×
761
                return nil
×
762
        }
×
763

764
        // Compute the name of the LSP for that endpoint target
765
        lspName, err := c.getEndpointTargetLSPName(pod, addresses)
×
766
        if err != nil {
×
767
                return fmt.Errorf("couldn't get LSP for the endpoint's target: %w", err)
×
768
        }
×
769

770
        for _, address := range addresses {
×
771
                key := address
×
772
                if util.CheckProtocol(address) == kubeovnv1.ProtocolIPv6 {
×
773
                        key = fmt.Sprintf("[%s]", address)
×
774
                }
×
775
                mapping[key] = fmt.Sprintf(util.HealthCheckNamedVipTemplate, lspName, checkVip)
×
776
        }
777

778
        return nil
×
779
}
780

781
// getIPPortMapping returns the mapping between each endpoint, LSP and health check VIP
782
func (c *Controller) getIPPortMapping(endpointSlices []*discoveryv1.EndpointSlice, service *v1.Service, checkVip string) (IPPortMapping, error) {
×
783
        // Choose the most optimized and straightforward way to compute the IPPortMapping
×
784
        if serviceHasSelector(service) {
×
785
                // The service has a selector, which means that the EndpointSlices should have targets.
×
786
                // We can use those targets instead of looking at every pod in the namespace.
×
787
                return c.getIPPortMappingWithTargets(endpointSlices, checkVip), nil
×
788
        }
×
789

790
        // The service has no selectors, we must find which pods in the namespace of the service
791
        // are targeted by the endpoint by only looking at the IPs.
792
        pods, err := c.podsLister.Pods(service.Namespace).List(labels.Everything())
×
793
        if err != nil {
×
794
                err := fmt.Errorf("failed to get pods for service %s in namespace %s: %w", service.Name, service.Namespace, err)
×
795
                klog.Error(err)
×
796
                return nil, err
×
797
        }
×
798

799
        return c.getIPPortMappingWithNoTargets(endpointSlices, pods, checkVip), nil
×
800
}
801

802
// getIPPortMappingWithTargets returns the IPPortMapping for endpoints with targets
803
func (c *Controller) getIPPortMappingWithTargets(endpointSlices []*discoveryv1.EndpointSlice, checkVip string) IPPortMapping {
×
804
        mapping := make(IPPortMapping)
×
805

×
806
        for _, slice := range endpointSlices {
×
807
                for _, endpoint := range slice.Endpoints {
×
808
                        if endpoint.TargetRef == nil {
×
809
                                continue
×
810
                        }
811

812
                        namespace, name := endpoint.TargetRef.Namespace, endpoint.TargetRef.Name
×
813
                        if name == "" || namespace == "" {
×
814
                                continue
×
815
                        }
816

817
                        // Retrieve the pod for that endpoint target
818
                        pod, err := c.podsLister.Pods(namespace).Get(name)
×
819
                        if err != nil {
×
820
                                err := fmt.Errorf("couldn't retrieve pod %s/%s: %w", namespace, name, err)
×
821
                                klog.Error(err)
×
822
                                continue
×
823
                        }
824

825
                        // Compute the IPPortMapping for that endpoint target
826
                        if err := c.addIPPortMappingEntry(pod, endpoint.Addresses, checkVip, mapping); err != nil {
×
827
                                err := fmt.Errorf("couldn't compute ip port mapping for pod %s/%s: %w", namespace, name, err)
×
828
                                klog.Error(err)
×
829
                                continue
×
830
                        }
831
                }
832
        }
833

834
        return mapping
×
835
}
836

837
// getIPPortMappingWithNoTargets returns the IPPortMapping for endpoints with no targets
838
func (c *Controller) getIPPortMappingWithNoTargets(endpointSlices []*discoveryv1.EndpointSlice, pods []*v1.Pod, checkVip string) IPPortMapping {
×
839
        mapping := make(IPPortMapping)
×
840

×
841
        for _, slice := range endpointSlices {
×
842
                for _, endpoint := range slice.Endpoints {
×
843
                        for _, pod := range pods {
×
844
                                // Try to find a matching provider for the addresses
×
845
                                provider, err := c.getEndpointProvider(pod, endpoint.Addresses)
×
846
                                if err != nil {
×
847
                                        err := fmt.Errorf("couldn't get provider for pod %s/%s: %w", pod.Namespace, pod.Name, err)
×
848
                                        klog.Error(err)
×
849
                                        continue
×
850
                                }
851

852
                                // If the pod has a provider that matches that set of addresses, it is an endpoint target.
853
                                // Otherwise, it isn't targeted by the EndpointSlice and can be dismissed.
854
                                if provider == "" {
×
855
                                        continue
×
856
                                }
857

858
                                // Compute the IPPortMapping for that endpoint target
859
                                if err := c.addIPPortMappingEntry(pod, endpoint.Addresses, checkVip, mapping); err != nil {
×
860
                                        err := fmt.Errorf("couldn't compute ip port mapping for pod %s/%s: %w", pod.Namespace, pod.Name, err)
×
861
                                        klog.Error(err)
×
862
                                        continue
×
863
                                }
864
                        }
865
                }
866
        }
867

868
        return mapping
×
869
}
870

871
// getPodProviders returns all the providers available on a pod
872
func (c *Controller) getPodProviders(pod *v1.Pod) ([]string, error) {
1✔
873
        // Get all the networks to which the pod is attached
1✔
874
        podNetworks, err := c.getPodKubeovnNets(pod)
1✔
875
        if err != nil {
1✔
876
                return nil, fmt.Errorf("failed to get pod networks: %w", err)
×
877
        }
×
878

879
        // Retrieve all the providers
880
        var providers []string
1✔
881
        for _, podNetwork := range podNetworks {
2✔
882
                providers = append(providers, podNetwork.ProviderName)
1✔
883
        }
1✔
884

885
        return providers, nil
1✔
886
}
887

888
// getMatchingProviderForAddress returns the provider linked to a subnet in which a particular address is present
889
func getMatchingProviderForAddress(pod *v1.Pod, providers []string, address string) string {
1✔
890
        if pod.Annotations == nil {
2✔
891
                return ""
1✔
892
        }
1✔
893

894
        // Find which provider is linked to this address
895
        for _, provider := range providers {
2✔
896
                ipsForProvider, exists := pod.Annotations[fmt.Sprintf(util.IPAddressAnnotationTemplate, provider)]
1✔
897
                if !exists {
1✔
898
                        continue
×
899
                }
900

901
                ips := strings.Split(ipsForProvider, ",")
1✔
902
                if slices.Contains(ips, address) {
2✔
903
                        return provider
1✔
904
                }
1✔
905
        }
906

907
        return ""
1✔
908
}
909

910
// getEndpointProvider returns the provider linked to the addresses of an endpoint
911
func (c *Controller) getEndpointProvider(pod *v1.Pod, addresses []string) (string, error) {
×
912
        // Retrieve all the providers of the pod
×
913
        providers, err := c.getPodProviders(pod)
×
914
        if err != nil {
×
915
                return "", err
×
916
        }
×
917

918
        // Get the first matching provider for any of the address in the endpoint
919
        var provider string
×
920
        for _, address := range addresses {
×
921
                if provider = getMatchingProviderForAddress(pod, providers, address); provider != "" {
×
922
                        return provider, nil
×
923
                }
×
924
        }
925

926
        return "", nil
×
927
}
928

929
// getEndpointTargetLSPNameFromProvider returns the name of the LSP for a pod targeted by an endpoint.
930
// A custom provider can be specified if the LSP is within a subnet that doesn't use
931
// the default "ovn" provider.
932
func getEndpointTargetLSPNameFromProvider(pod *v1.Pod, provider string) string {
1✔
933
        // If no provider is specified, use the default one
1✔
934
        if provider == "" {
2✔
935
                provider = util.OvnProvider
1✔
936
        }
1✔
937

938
        target := pod.Name
1✔
939

1✔
940
        // If this pod is a VM launcher pod, we need to retrieve the name of the VM. This is necessary
1✔
941
        // because we do not use the same syntax for the LSP of normal pods and for VM pods
1✔
942
        if vmName, exists := pod.Annotations[fmt.Sprintf(util.VMAnnotationTemplate, provider)]; exists {
2✔
943
                target = vmName
1✔
944
        }
1✔
945

946
        return ovs.PodNameToPortName(target, pod.Namespace, provider)
1✔
947
}
948

949
// getEndpointTargetLSP returns the name of the LSP on which addresses are attached for a specific pod
950
func (c *Controller) getEndpointTargetLSPName(pod *v1.Pod, addresses []string) (string, error) {
×
951
        // Retrieve the provider for those addresses
×
952
        provider, err := c.getEndpointProvider(pod, addresses)
×
953
        if err != nil {
×
954
                return "", err
×
955
        }
×
956

957
        return getEndpointTargetLSPNameFromProvider(pod, provider), nil
×
958
}
959

960
// getSubnetByProvider returns the subnet linked to a provider on a pod
961
func getSubnetByProvider(pod *v1.Pod, provider string) (string, error) {
×
962
        subnetName, exists := pod.Annotations[fmt.Sprintf(util.LogicalSwitchAnnotationTemplate, provider)]
×
963
        if !exists {
×
964
                return "", fmt.Errorf("couldn't find subnet linked to provider %s", provider)
×
965
        }
×
966

967
        return subnetName, nil
×
968
}
969

970
// getVpcByProvider returns the VPC linked to a provider on a pod.
971
// For underlay subnets without LogicalGateway or U2OInterconnection,
972
// the logical_router annotation is not set, so an empty string is returned.
973
func getVpcByProvider(pod *v1.Pod, provider string) string {
×
974
        return pod.Annotations[fmt.Sprintf(util.LogicalRouterAnnotationTemplate, provider)]
×
975
}
×
976

977
// getEndpointVpcAndSubnet returns the VPC/subnet for a pod and a set of addresses attached to it
978
func (c *Controller) getEndpointVpcAndSubnet(pod *v1.Pod, addresses []string) (string, string, error) {
×
979
        // Retrieve the provider for those addresses
×
980
        provider, err := c.getEndpointProvider(pod, addresses)
×
981
        if err != nil {
×
982
                return "", "", err
×
983
        }
×
984

985
        if provider == "" {
×
986
                return "", "", nil
×
987
        }
×
988

989
        // Retrieve the subnet
990
        subnet, err := getSubnetByProvider(pod, provider)
×
991
        if err != nil {
×
992
                return "", "", err
×
993
        }
×
994

995
        // Retrieve the VPC
996
        vpc := getVpcByProvider(pod, provider)
×
997

×
998
        return vpc, subnet, nil
×
999
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc