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

kubeovn / kube-ovn / 30339858760

28 Jul 2026 07:49AM UTC coverage: 25.914% (+0.08%) from 25.836%
30339858760

push

github

web-flow
[release-1.16] fix(metallb): preserve internal underlay VIP traffic (#7067)

* fix(metallb): preserve internal underlay VIP traffic

Signed-off-by: clyi <clyi@alauda.io>

* fix(metallb): constrain underlay VIP local backends

Signed-off-by: clyi <clyi@alauda.io>

* ci: run metallb e2e with three nodes

Signed-off-by: clyi <clyi@alauda.io>

---------

Signed-off-by: clyi <clyi@alauda.io>

113 of 282 new or added lines in 6 files covered. (40.07%)

1 existing line in 1 file now uncovered.

14889 of 57455 relevant lines covered (25.91%)

0.6 hits per line

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

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

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

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

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

24
type IPPortMapping map[string]string
25

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

32
        return ""
2✔
33
}
34

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

41
        return endpointSlice.Namespace + "/" + service
2✔
42
}
43

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

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

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

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

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

71
        key := findServiceKey(newEndpointSlice)
2✔
72
        if key != "" {
4✔
73
                klog.V(3).Infof("enqueue update endpointSlice for service %s", key)
2✔
74
                c.addOrUpdateEndpointSliceQueue.Add(key)
2✔
75
        }
2✔
76
}
77

78
func (c *Controller) handleUpdateEndpointSlice(key string) error {
×
79
        namespace, name, err := cache.SplitMetaNamespaceKey(key)
×
80
        if err != nil {
×
81
                utilruntime.HandleError(fmt.Errorf("invalid resource key: %s", key))
×
82
                return nil
×
83
        }
×
84

85
        c.epKeyMutex.LockKey(key)
×
86
        defer func() { _ = c.epKeyMutex.UnlockKey(key) }()
×
87
        klog.Infof("handle update endpointSlice for service %s", key)
×
88

×
89
        endpointSlices, err := c.endpointSlicesLister.EndpointSlices(namespace).List(labels.Set{discoveryv1.LabelServiceName: name}.AsSelector())
×
90
        if err != nil {
×
91
                if errors.IsNotFound(err) {
×
92
                        return nil
×
93
                }
×
94
                klog.Error(err)
×
95
                return err
×
96
        }
97

98
        cachedService, err := c.servicesLister.Services(namespace).Get(name)
×
99
        if err != nil {
×
100
                if errors.IsNotFound(err) {
×
101
                        return nil
×
102
                }
×
103
                klog.Error(err)
×
104
                return err
×
105
        }
106
        svc := cachedService.DeepCopy()
×
107

×
108
        var (
×
109
                lbVips                   []string
×
110
                vip, vpcName, subnetName string
×
NEW
111
                externalVIPNode          string
×
NEW
112
                serviceL2StatusReady     = true
×
113
                ok                       bool
×
114
                ignoreHealthCheck        = true
×
115
                isPreferLocalBackend     = false
×
116
        )
×
117

×
118
        if vip, ok = svc.Annotations[util.SwitchLBRuleVipsAnnotation]; ok && vip != "" {
×
119
                lbVips = []string{vip}
×
120

×
121
                // Health checks can only run against IPv4 endpoints and if the service doesn't specify they must be disabled
×
122
                if util.CheckProtocol(vip) == kubeovnv1.ProtocolIPv4 && !serviceHealthChecksDisabled(svc) {
×
123
                        ignoreHealthCheck = false
×
124
                }
×
125
        } else if lbVips = util.ServiceClusterIPs(*svc); len(lbVips) == 0 {
×
126
                return nil
×
127
        }
×
128

129
        if c.config.EnableLb && c.config.EnableOVNLBPreferLocal {
×
NEW
130
                if svc.Spec.Type == v1.ServiceTypeLoadBalancer {
×
NEW
131
                        for _, ingress := range svc.Status.LoadBalancer.Ingress {
×
NEW
132
                                if ingress.IP != "" {
×
NEW
133
                                        lbVips = append(lbVips, ingress.IP)
×
NEW
134
                                }
×
135
                        }
NEW
136
                        if svc.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeLocal {
×
NEW
137
                                isPreferLocalBackend = true
×
NEW
138
                                externalVIPNode, serviceL2StatusReady, err = c.getServiceL2StatusNode(namespace, name)
×
NEW
139
                                if err != nil {
×
NEW
140
                                        return err
×
UNCOV
141
                                }
×
142
                        }
143
                } else if svc.Spec.Type == v1.ServiceTypeClusterIP && svc.Spec.InternalTrafficPolicy != nil && *svc.Spec.InternalTrafficPolicy == v1.ServiceInternalTrafficPolicyLocal {
×
144
                        isPreferLocalBackend = true
×
145
                }
×
146
        }
147

148
        // If Kube-OVN is running in secondary CNI mode, the endpoint IPs should be derived from the network attachment definitions
149
        // This overwrite can be removed if endpoint construction accounts for network attachment IP address
150
        // TODO: Identify how endpoints are constructed, by default, endpoints has IP address of eth0 interface
151
        if c.config.EnableNonPrimaryCNI && serviceHasSelector(svc) {
×
152
                var pods []*v1.Pod
×
153
                if pods, err = c.podsLister.Pods(namespace).List(labels.Set(svc.Spec.Selector).AsSelector()); err != nil {
×
154
                        klog.Errorf("failed to get pods for service %s in namespace %s: %v", name, namespace, err)
×
155
                        return err
×
156
                }
×
157
                err = c.replaceEndpointAddressesWithSecondaryIPs(endpointSlices, pods)
×
158
                if err != nil {
×
159
                        klog.Errorf("failed to update endpointSlice: %v", err)
×
160
                        return err
×
161
                }
×
162
        }
163

164
        vpcName, subnetName, err = c.getVpcAndSubnetForEndpoints(endpointSlices, svc)
×
165
        if err != nil {
×
166
                return err
×
167
        }
×
168

169
        var (
×
170
                vpc    *kubeovnv1.Vpc
×
171
                svcVpc string
×
172
        )
×
173

×
174
        if vpc, err = c.vpcsLister.Get(vpcName); err != nil {
×
175
                klog.Errorf("failed to get vpc %s, %v", vpcName, err)
×
176
                return err
×
177
        }
×
178

179
        tcpLb, udpLb, sctpLb := vpc.Status.TCPLoadBalancer, vpc.Status.UDPLoadBalancer, vpc.Status.SctpLoadBalancer
×
180
        oldTCPLb, oldUDPLb, oldSctpLb := vpc.Status.TCPSessionLoadBalancer, vpc.Status.UDPSessionLoadBalancer, vpc.Status.SctpSessionLoadBalancer
×
181
        if svc.Spec.SessionAffinity == v1.ServiceAffinityClientIP {
×
182
                tcpLb, udpLb, sctpLb, oldTCPLb, oldUDPLb, oldSctpLb = oldTCPLb, oldUDPLb, oldSctpLb, tcpLb, udpLb, sctpLb
×
183
        }
×
NEW
184
        if c.config.EnableOVNLBPreferLocal {
×
NEW
185
                if err = c.clearLoadBalancerVIPExternalTrafficLocal(svc, tcpLb, udpLb, sctpLb); err != nil {
×
NEW
186
                        return err
×
NEW
187
                }
×
NEW
188
                if err = c.clearLoadBalancerVIPExternalTrafficLocal(svc, oldTCPLb, oldUDPLb, oldSctpLb); err != nil {
×
NEW
189
                        return err
×
NEW
190
                }
×
191
        }
192

193
        for _, lbVip := range lbVips {
×
194
                for _, port := range svc.Spec.Ports {
×
195
                        var lb, oldLb string
×
196
                        switch port.Protocol {
×
197
                        case v1.ProtocolTCP:
×
198
                                lb, oldLb = tcpLb, oldTCPLb
×
199
                        case v1.ProtocolUDP:
×
200
                                lb, oldLb = udpLb, oldUDPLb
×
201
                        case v1.ProtocolSCTP:
×
202
                                lb, oldLb = sctpLb, oldSctpLb
×
203
                        }
204

205
                        var (
×
206
                                vip, checkIP             string
×
207
                                backends                 []string
×
208
                                ipPortMapping, externals map[string]string
×
209
                        )
×
210

×
211
                        if !ignoreHealthCheck {
×
212
                                if checkIP, err = c.getHealthCheckVip(subnetName, lbVip); err != nil {
×
213
                                        klog.Error(err)
×
214
                                        return err
×
215
                                }
×
216
                                externals = map[string]string{
×
217
                                        util.SwitchLBRuleSubnet: subnetName,
×
218
                                }
×
219
                        }
220

221
                        if isPreferLocalBackend {
×
222
                                // only use the ipportmapping's lsp to ip map when the backend is local
×
223
                                checkIP = util.MasqueradeCheckIP
×
224
                        }
×
225

226
                        backends = c.getEndpointBackend(endpointSlices, port, lbVip)
×
227

×
228
                        if !ignoreHealthCheck || isPreferLocalBackend {
×
229
                                ipPortMapping, err = c.getIPPortMapping(endpointSlices, svc, checkIP)
×
230
                                if err != nil {
×
231
                                        err := fmt.Errorf("couldn't get ip port mapping for svc %s/%s: %w", svc.Namespace, svc.Name, err)
×
232
                                        return err
×
233
                                }
×
234
                        }
235

236
                        // for performance reason delete lb with no backends
237
                        if len(backends) != 0 {
×
238
                                vip = util.JoinHostPort(lbVip, port.Port)
×
239
                                klog.Infof("add vip endpoint %s, backends %v to LB %s", vip, backends, lb)
×
240
                                if err = c.OVNNbClient.LoadBalancerAddVip(lb, vip, backends...); err != nil {
×
241
                                        klog.Errorf("failed to add vip %s with backends %s to LB %s: %v", lbVip, backends, lb, err)
×
242
                                        return err
×
243
                                }
×
NEW
244
                                if isPreferLocalBackend &&
×
NEW
245
                                        svc.Spec.Type == v1.ServiceTypeLoadBalancer &&
×
NEW
246
                                        svc.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeLocal &&
×
NEW
247
                                        serviceL2StatusReady &&
×
NEW
248
                                        slices.ContainsFunc(svc.Status.LoadBalancer.Ingress, func(ingress v1.LoadBalancerIngress) bool {
×
NEW
249
                                                return ingress.IP == lbVip
×
NEW
250
                                        }) {
×
NEW
251
                                        vipNodeLSP := ""
×
NEW
252
                                        if externalVIPNode != "" {
×
NEW
253
                                                vipNodeLSP = util.NodeLspName(externalVIPNode)
×
NEW
254
                                        }
×
NEW
255
                                        if err = c.OVNNbClient.SetLoadBalancerVIPExternalTrafficLocal(lb, vip, vipNodeLSP); err != nil {
×
NEW
256
                                                return fmt.Errorf("couldn't mark external local vip %s on LB %s: %w", vip, lb, err)
×
NEW
257
                                        }
×
258
                                }
259

260
                                if isPreferLocalBackend && len(ipPortMapping) != 0 {
×
261
                                        if err = c.OVNNbClient.LoadBalancerUpdateIPPortMapping(lb, vip, ipPortMapping); err != nil {
×
262
                                                klog.Errorf("failed to update ip port mapping %s for vip %s to LB %s: %v", ipPortMapping, vip, lb, err)
×
263
                                                return err
×
264
                                        }
×
265
                                }
266

267
                                if !ignoreHealthCheck {
×
268
                                        klog.Infof("add health check ip port mapping %v to LB %s", ipPortMapping, lb)
×
269
                                        if err = c.OVNNbClient.LoadBalancerAddHealthCheck(lb, vip, ignoreHealthCheck, ipPortMapping, externals); err != nil {
×
270
                                                klog.Errorf("failed to add health check for vip %s with ip port mapping %s to LB %s: %v", lbVip, ipPortMapping, lb, err)
×
271
                                                return err
×
272
                                        }
×
273
                                }
274
                        } else {
×
275
                                vip = util.JoinHostPort(lbVip, port.Port)
×
276
                                klog.V(3).Infof("delete vip endpoint %s from LB %s", vip, lb)
×
277
                                if err = c.OVNNbClient.LoadBalancerDeleteVip(lb, vip, true); err != nil {
×
278
                                        klog.Errorf("failed to delete vip endpoint %s from LB %s: %v", vip, lb, err)
×
279
                                        return err
×
280
                                }
×
281

282
                                klog.V(3).Infof("delete vip endpoint %s from old LB %s", vip, oldLb)
×
283
                                if err = c.OVNNbClient.LoadBalancerDeleteVip(oldLb, vip, true); err != nil {
×
284
                                        klog.Errorf("failed to delete vip %s from LB %s: %v", vip, oldLb, err)
×
285
                                        return err
×
286
                                }
×
287

288
                                if c.config.EnableOVNLBPreferLocal {
×
289
                                        if err := c.OVNNbClient.LoadBalancerDeleteIPPortMapping(lb, vip); err != nil {
×
290
                                                klog.Errorf("failed to delete ip port mapping for vip %s from LB %s: %v", vip, lb, err)
×
291
                                                return err
×
292
                                        }
×
293
                                        if err := c.OVNNbClient.LoadBalancerDeleteIPPortMapping(oldLb, vip); err != nil {
×
294
                                                klog.Errorf("failed to delete ip port mapping for vip %s from LB %s: %v", vip, lb, err)
×
295
                                                return err
×
296
                                        }
×
297
                                }
298
                        }
299
                }
300
        }
301

302
        if svcVpc = svc.Annotations[util.VpcAnnotation]; svcVpc != vpcName {
×
303
                patch := util.KVPatch{util.VpcAnnotation: vpcName}
×
304
                if err = util.PatchAnnotations(c.config.KubeClient.CoreV1().Services(namespace), svc.Name, patch); err != nil {
×
305
                        klog.Errorf("failed to patch service %s: %v", key, err)
×
306
                        return err
×
307
                }
×
308
        }
309

310
        return nil
×
311
}
312

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

385
        return nil
2✔
386
}
387

388
func (c *Controller) clearLoadBalancerVIPExternalTrafficLocal(svc *v1.Service, tcpLb, udpLb, sctpLb string) error {
2✔
389
        if svc.Spec.Type != v1.ServiceTypeLoadBalancer ||
2✔
390
                svc.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeLocal {
4✔
391
                return nil
2✔
392
        }
2✔
393

394
        for _, ingress := range svc.Status.LoadBalancer.Ingress {
4✔
395
                if ingress.IP == "" {
2✔
NEW
396
                        continue
×
397
                }
398
                for _, port := range svc.Spec.Ports {
4✔
399
                        var lb string
2✔
400
                        switch port.Protocol {
2✔
401
                        case v1.ProtocolTCP:
2✔
402
                                lb = tcpLb
2✔
NEW
403
                        case v1.ProtocolUDP:
×
NEW
404
                                lb = udpLb
×
NEW
405
                        case v1.ProtocolSCTP:
×
NEW
406
                                lb = sctpLb
×
407
                        }
408
                        if lb == "" {
2✔
NEW
409
                                continue
×
410
                        }
411
                        vip := util.JoinHostPort(ingress.IP, port.Port)
2✔
412
                        if err := c.OVNNbClient.SetLoadBalancerVIPExternalTrafficLocal(lb, vip, ""); err != nil {
2✔
NEW
413
                                return fmt.Errorf("couldn't clear external local vip marker %s on LB %s: %w", vip, lb, err)
×
NEW
414
                        }
×
415
                        if err := c.OVNNbClient.LoadBalancerDeleteIPPortMapping(lb, vip); err != nil {
2✔
NEW
416
                                return fmt.Errorf("couldn't clear external local vip ip port mapping %s on LB %s: %w", vip, lb, err)
×
NEW
417
                        }
×
418
                }
419
        }
420
        return nil
2✔
421
}
422

423
// enqueueStaticEndpointUpdateInNamespace enqueues updates for every statically generated EndpointSlice in a namespace.
424
// Statically generated EndpointSlices are not generated by the selectors of their parent service.
425
func (c *Controller) enqueueStaticEndpointUpdateInNamespace(namespace string) {
×
426
        // Find all the statically generated EndpointSlices in the namespace
×
427
        endpointSlices, err := c.findStaticEndpointSlicesInNamespace(namespace)
×
428
        if err != nil {
×
429
                err := fmt.Errorf("couldn't find static endpointslices in namespace %s: %w", namespace, err)
×
430
                klog.Error(err)
×
431
        }
×
432

433
        // Enqueue updates for all the EndpointSlices
434
        for _, slice := range endpointSlices {
×
435
                c.enqueueAddEndpointSlice(slice)
×
436
        }
×
437
}
438

439
// serviceHealthChecksDisabled returns whether health checks must be omitted for a particular service
440
func serviceHealthChecksDisabled(service *v1.Service) bool {
2✔
441
        // Service must not have disabled health checks
2✔
442
        if service.Annotations != nil && service.Annotations[util.ServiceHealthCheck] == "false" {
4✔
443
                return true
2✔
444
        }
2✔
445

446
        // If nothing is specified, checks are enabled by default
447
        return false
2✔
448
}
449

450
// findStaticEndpointSlicesInNamespace finds all the EndpointSlices in a namespace that are statically generated.
451
// Statically generated EndpointSlices are not generated by the selectors of their parent service.
452
func (c *Controller) findStaticEndpointSlicesInNamespace(namespace string) ([]*discoveryv1.EndpointSlice, error) {
×
453
        // Retrieve all the services in the namespace
×
454
        services, err := c.servicesLister.Services(namespace).List(labels.Everything())
×
455
        if err != nil {
×
456
                err := fmt.Errorf("couldn't list services in namespace %s: %w", namespace, err)
×
457
                klog.Error(err)
×
458
                return nil, err
×
459
        }
×
460

461
        // Only handle services that have static endpoints provided, and not selectors
462
        var filteredServices []*v1.Service
×
463
        for _, service := range services {
×
464
                if serviceHasSelector(service) {
×
465
                        continue
×
466
                }
467

468
                filteredServices = append(filteredServices, service)
×
469
        }
470

471
        // Find the EndpointSlices linked to those services
472
        endpointSlices, err := c.findEndpointSlicesForServices(namespace, filteredServices)
×
473
        if err != nil {
×
474
                return nil, err
×
475
        }
×
476

477
        return endpointSlices, nil
×
478
}
479

480
// findEndpointSlicesForServices returns all the EndpointSlices that are linked to services in the same namespace.
481
// Parameter "namespace" is the namespace in which all the services are located.
482
// Parameter "services" is a list of all the services for which we want to find the EndpointSlices.
483
func (c *Controller) findEndpointSlicesForServices(namespace string, services []*v1.Service) ([]*discoveryv1.EndpointSlice, error) {
×
484
        var endpointSlices []*discoveryv1.EndpointSlice
×
485

×
486
        // Retrieve all the endpointSlices in the namespace of the services
×
487
        eps, err := c.endpointSlicesLister.EndpointSlices(namespace).List(labels.Everything())
×
488
        if err != nil {
×
489
                err := fmt.Errorf("couldn't list endpointslices in namespace %s: %w", namespace, err)
×
490
                klog.Error(err)
×
491
                return nil, err
×
492
        }
×
493

494
        // Find the EndpointSlices part of each service
495
        for _, service := range services {
×
496
                for _, endpointSlice := range eps {
×
497
                        if getServiceForEndpointSlice(endpointSlice) == service.Name {
×
498
                                endpointSlices = append(endpointSlices, endpointSlice)
×
499
                        }
×
500
                }
501
        }
502

503
        return endpointSlices, nil
×
504
}
505

506
// serviceHasSelector returns if a service has selectors
507
func serviceHasSelector(service *v1.Service) bool {
2✔
508
        return len(service.Spec.Selector) > 0
2✔
509
}
2✔
510

511
// getCustomServiceVpcAndSubnet returns the custom VPC/Subnet defined on a service
512
func getCustomServiceVpcAndSubnet(service *v1.Service) (vpcName, subnetName string) {
×
513
        if service.Annotations != nil {
×
514
                vpcName = service.Annotations[util.LogicalRouterAnnotation]
×
515
                subnetName = service.Annotations[util.LogicalSwitchAnnotation]
×
516
        }
×
517

518
        return vpcName, subnetName
×
519
}
520

521
// getDefaultVpcAndSubnet returns the default VPC/Subnet to apply to a LoadBalancer if nothing was found
522
// during automatic discovery. If both parameters are non-empty, they are returned as is.
523
func (c *Controller) getDefaultVpcAndSubnet(service *v1.Service, vpcName, subnetName string) (string, string) {
×
524
        // Default to what's on the service or to the default VPC
×
525
        if vpcName == "" {
×
526
                if vpcName = service.Annotations[util.VpcAnnotation]; vpcName == "" {
×
527
                        vpcName = c.config.ClusterRouter
×
528
                }
×
529
        }
530

531
        // Use the default subnet if it wasn't found
532
        if subnetName == "" {
×
533
                subnetName = util.DefaultSubnet
×
534
        }
×
535

536
        return vpcName, subnetName
×
537
}
538

539
// getVpcAndSubnetForEndpoints returns the name of the VPC/Subnet for EndpointSlices
540
func (c *Controller) getVpcAndSubnetForEndpoints(endpointSlices []*discoveryv1.EndpointSlice, service *v1.Service) (vpcName, subnetName string, err error) {
×
541
        // Let the user self-determine what VPC and subnet to use if they provided annotations on the service
×
542
        // Both the VPC and Subnet must be provided
×
543
        vpcName, subnetName = getCustomServiceVpcAndSubnet(service)
×
544
        if vpcName != "" && subnetName != "" {
×
545
                return vpcName, subnetName, nil
×
546
        }
×
547

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

563
                vpcName, subnetName = c.findVpcAndSubnetWithNoTargets(endpointSlices, pods)
×
564
        }
565

566
        vpcName, subnetName = c.getDefaultVpcAndSubnet(service, vpcName, subnetName)
×
567
        return vpcName, subnetName, nil
×
568
}
569

570
// findVpcAndSubnetWithTargets returns the name of the VPC and Subnet for endpoints with targets
571
func (c *Controller) findVpcAndSubnetWithTargets(endpointSlices []*discoveryv1.EndpointSlice) (vpcName, subnetName string) {
×
572
        for _, slice := range endpointSlices {
×
573
                for _, endpoint := range slice.Endpoints {
×
574
                        if endpoint.TargetRef == nil {
×
575
                                continue
×
576
                        }
577

578
                        namespace, name := endpoint.TargetRef.Namespace, endpoint.TargetRef.Name
×
579
                        if name == "" || namespace == "" {
×
580
                                continue
×
581
                        }
582

583
                        pod, err := c.podsLister.Pods(namespace).Get(name)
×
584
                        if err != nil {
×
585
                                err := fmt.Errorf("couldn't retrieve pod %s/%s: %w", namespace, name, err)
×
586
                                klog.Error(err)
×
587
                                continue
×
588
                        }
589

590
                        vpc, subnet, err := c.getEndpointVpcAndSubnet(pod, endpoint.Addresses)
×
591
                        if err != nil {
×
592
                                err := fmt.Errorf("couldn't retrieve subnet/vpc for pod %s/%s: %w", namespace, name, err)
×
593
                                klog.Error(err)
×
594
                                continue
×
595
                        }
596

597
                        if vpcName == "" {
×
598
                                vpcName = vpc
×
599
                        }
×
600

601
                        if subnetName == "" {
×
602
                                subnetName = subnet
×
603
                        }
×
604

605
                        if vpcName != "" && subnetName != "" {
×
606
                                return vpcName, subnetName
×
607
                        }
×
608
                }
609
        }
610

611
        return vpcName, subnetName
×
612
}
613

614
// findVpcAndSubnetWithNoTargets returns the name of the VPC and Subnet for endpoints with no targets
615
func (c *Controller) findVpcAndSubnetWithNoTargets(endpointSlices []*discoveryv1.EndpointSlice, pods []*v1.Pod) (vpcName, subnetName string) {
×
616
        for _, slice := range endpointSlices {
×
617
                for _, endpoint := range slice.Endpoints {
×
618
                        for _, pod := range pods {
×
619
                                vpc, subnet, err := c.getEndpointVpcAndSubnet(pod, endpoint.Addresses)
×
620
                                if err != nil {
×
621
                                        err := fmt.Errorf("couldn't retrieve subnet/vpc for pod %s/%s: %w", pod.Namespace, pod.Name, err)
×
622
                                        klog.Error(err)
×
623
                                        continue
×
624
                                }
625

626
                                if vpcName == "" {
×
627
                                        vpcName = vpc
×
628
                                }
×
629

630
                                if subnetName == "" {
×
631
                                        subnetName = subnet
×
632
                                }
×
633

634
                                if vpcName != "" && subnetName != "" {
×
635
                                        return vpcName, subnetName
×
636
                                }
×
637
                        }
638
                }
639
        }
640

641
        return vpcName, subnetName
×
642
}
643

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

677
                // wait for vip created
678
                // TODO: WATCH VIP
679
                time.Sleep(1 * time.Second)
×
680
                checkVip, err = c.virtualIpsLister.Get(vipName)
×
681
                if err != nil {
×
682
                        klog.Errorf("failed to get health check vip %s, %v", vipName, err)
×
683
                        return "", err
×
684
                }
×
685
        }
686

687
        if checkVip.Status.V4ip == "" && checkVip.Status.V6ip == "" {
×
688
                err = fmt.Errorf("vip %s is not ready", vipName)
×
689
                klog.Error(err)
×
690
                return "", err
×
691
        }
×
692

693
        switch util.CheckProtocol(lbVip) {
×
694
        case kubeovnv1.ProtocolIPv4:
×
695
                checkIP = checkVip.Status.V4ip
×
696
        case kubeovnv1.ProtocolIPv6:
×
697
                checkIP = checkVip.Status.V6ip
×
698
        }
699
        if checkIP == "" {
×
700
                err = fmt.Errorf("failed to get health check vip subnet %s", vipName)
×
701
                klog.Error(err)
×
702
                return "", err
×
703
        }
×
704

705
        return checkIP, nil
×
706
}
707

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

×
712
        for _, endpointSlice := range endpointSlices {
×
713
                var targetPort int32
×
714
                for _, port := range endpointSlice.Ports {
×
715
                        if port.Name != nil && *port.Name == servicePort.Name {
×
716
                                targetPort = *port.Port
×
717
                                break
×
718
                        }
719
                }
720
                if targetPort == 0 {
×
721
                        continue
×
722
                }
723

724
                for _, endpoint := range endpointSlice.Endpoints {
×
725
                        if !endpointReady(endpoint) {
×
726
                                continue
×
727
                        }
728

729
                        for _, address := range endpoint.Addresses {
×
730
                                if util.CheckProtocol(address) == protocol {
×
731
                                        backends = append(backends, util.JoinHostPort(address, targetPort))
×
732
                                }
×
733
                        }
734
                }
735
        }
736

737
        return backends
×
738
}
739

740
// endpointReady returns whether an endpoint can receive traffic
741
func endpointReady(endpoint discoveryv1.Endpoint) bool {
2✔
742
        return endpoint.Conditions.Ready == nil || *endpoint.Conditions.Ready
2✔
743
}
2✔
744

745
// addIPPortMappingEntry adds a new entry to an IPPortMapping for a given target, the addresses on that target and the
746
// VIP used to run the health checks
747
func (c *Controller) addIPPortMappingEntry(pod *v1.Pod, addresses []string, checkVip string, mapping IPPortMapping) error {
×
748
        // Abort if the pod is getting deleted
×
749
        if !pod.DeletionTimestamp.IsZero() {
×
750
                return nil
×
751
        }
×
752

753
        // Compute the name of the LSP for that endpoint target
754
        lspName, err := c.getEndpointTargetLSPName(pod, addresses)
×
755
        if err != nil {
×
756
                return fmt.Errorf("couldn't get LSP for the endpoint's target: %w", err)
×
757
        }
×
758

759
        for _, address := range addresses {
×
760
                key := address
×
761
                if util.CheckProtocol(address) == kubeovnv1.ProtocolIPv6 {
×
762
                        key = fmt.Sprintf("[%s]", address)
×
763
                }
×
764
                mapping[key] = fmt.Sprintf(util.HealthCheckNamedVipTemplate, lspName, checkVip)
×
765
        }
766

767
        return nil
×
768
}
769

770
// getIPPortMapping returns the mapping between each endpoint, LSP and health check VIP
771
func (c *Controller) getIPPortMapping(endpointSlices []*discoveryv1.EndpointSlice, service *v1.Service, checkVip string) (IPPortMapping, error) {
×
772
        // Choose the most optimized and straightforward way to compute the IPPortMapping
×
773
        if serviceHasSelector(service) {
×
774
                // The service has a selector, which means that the EndpointSlices should have targets.
×
775
                // We can use those targets instead of looking at every pod in the namespace.
×
776
                return c.getIPPortMappingWithTargets(endpointSlices, checkVip), nil
×
777
        }
×
778

779
        // The service has no selectors, we must find which pods in the namespace of the service
780
        // are targeted by the endpoint by only looking at the IPs.
781
        pods, err := c.podsLister.Pods(service.Namespace).List(labels.Everything())
×
782
        if err != nil {
×
783
                err := fmt.Errorf("failed to get pods for service %s in namespace %s: %w", service.Name, service.Namespace, err)
×
784
                klog.Error(err)
×
785
                return nil, err
×
786
        }
×
787

788
        return c.getIPPortMappingWithNoTargets(endpointSlices, pods, checkVip), nil
×
789
}
790

791
// getIPPortMappingWithTargets returns the IPPortMapping for endpoints with targets
792
func (c *Controller) getIPPortMappingWithTargets(endpointSlices []*discoveryv1.EndpointSlice, checkVip string) IPPortMapping {
×
793
        mapping := make(IPPortMapping)
×
794

×
795
        for _, slice := range endpointSlices {
×
796
                for _, endpoint := range slice.Endpoints {
×
797
                        if endpoint.TargetRef == nil {
×
798
                                continue
×
799
                        }
800

801
                        namespace, name := endpoint.TargetRef.Namespace, endpoint.TargetRef.Name
×
802
                        if name == "" || namespace == "" {
×
803
                                continue
×
804
                        }
805

806
                        // Retrieve the pod for that endpoint target
807
                        pod, err := c.podsLister.Pods(namespace).Get(name)
×
808
                        if err != nil {
×
809
                                err := fmt.Errorf("couldn't retrieve pod %s/%s: %w", namespace, name, err)
×
810
                                klog.Error(err)
×
811
                                continue
×
812
                        }
813

814
                        // Compute the IPPortMapping for that endpoint target
815
                        if err := c.addIPPortMappingEntry(pod, endpoint.Addresses, checkVip, mapping); err != nil {
×
816
                                err := fmt.Errorf("couldn't compute ip port mapping for pod %s/%s: %w", namespace, name, err)
×
817
                                klog.Error(err)
×
818
                                continue
×
819
                        }
820
                }
821
        }
822

823
        return mapping
×
824
}
825

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

×
830
        for _, slice := range endpointSlices {
×
831
                for _, endpoint := range slice.Endpoints {
×
832
                        for _, pod := range pods {
×
833
                                // Try to find a matching provider for the addresses
×
834
                                provider, err := c.getEndpointProvider(pod, endpoint.Addresses)
×
835
                                if err != nil {
×
836
                                        err := fmt.Errorf("couldn't get provider for pod %s/%s: %w", pod.Namespace, pod.Name, err)
×
837
                                        klog.Error(err)
×
838
                                        continue
×
839
                                }
840

841
                                // If the pod has a provider that matches that set of addresses, it is an endpoint target.
842
                                // Otherwise, it isn't targeted by the EndpointSlice and can be dismissed.
843
                                if provider == "" {
×
844
                                        continue
×
845
                                }
846

847
                                // Compute the IPPortMapping for that endpoint target
848
                                if err := c.addIPPortMappingEntry(pod, endpoint.Addresses, checkVip, mapping); err != nil {
×
849
                                        err := fmt.Errorf("couldn't compute ip port mapping for pod %s/%s: %w", pod.Namespace, pod.Name, err)
×
850
                                        klog.Error(err)
×
851
                                        continue
×
852
                                }
853
                        }
854
                }
855
        }
856

857
        return mapping
×
858
}
859

860
// getPodProviders returns all the providers available on a pod
861
func (c *Controller) getPodProviders(pod *v1.Pod) ([]string, error) {
2✔
862
        // Get all the networks to which the pod is attached
2✔
863
        podNetworks, err := c.getPodKubeovnNets(pod)
2✔
864
        if err != nil {
2✔
865
                return nil, fmt.Errorf("failed to get pod networks: %w", err)
×
866
        }
×
867

868
        // Retrieve all the providers
869
        var providers []string
2✔
870
        for _, podNetwork := range podNetworks {
4✔
871
                providers = append(providers, podNetwork.ProviderName)
2✔
872
        }
2✔
873

874
        return providers, nil
2✔
875
}
876

877
// getMatchingProviderForAddress returns the provider linked to a subnet in which a particular address is present
878
func getMatchingProviderForAddress(pod *v1.Pod, providers []string, address string) string {
2✔
879
        if pod.Annotations == nil {
4✔
880
                return ""
2✔
881
        }
2✔
882

883
        // Find which provider is linked to this address
884
        for _, provider := range providers {
4✔
885
                ipsForProvider, exists := pod.Annotations[fmt.Sprintf(util.IPAddressAnnotationTemplate, provider)]
2✔
886
                if !exists {
2✔
887
                        continue
×
888
                }
889

890
                ips := strings.Split(ipsForProvider, ",")
2✔
891
                if slices.Contains(ips, address) {
4✔
892
                        return provider
2✔
893
                }
2✔
894
        }
895

896
        return ""
2✔
897
}
898

899
// getEndpointProvider returns the provider linked to the addresses of an endpoint
900
func (c *Controller) getEndpointProvider(pod *v1.Pod, addresses []string) (string, error) {
×
901
        // Retrieve all the providers of the pod
×
902
        providers, err := c.getPodProviders(pod)
×
903
        if err != nil {
×
904
                return "", err
×
905
        }
×
906

907
        // Get the first matching provider for any of the address in the endpoint
908
        var provider string
×
909
        for _, address := range addresses {
×
910
                if provider = getMatchingProviderForAddress(pod, providers, address); provider != "" {
×
911
                        return provider, nil
×
912
                }
×
913
        }
914

915
        return "", nil
×
916
}
917

918
// getEndpointTargetLSPNameFromProvider returns the name of the LSP for a pod targeted by an endpoint.
919
// A custom provider can be specified if the LSP is within a subnet that doesn't use
920
// the default "ovn" provider.
921
func getEndpointTargetLSPNameFromProvider(pod *v1.Pod, provider string) string {
2✔
922
        // If no provider is specified, use the default one
2✔
923
        if provider == "" {
4✔
924
                provider = util.OvnProvider
2✔
925
        }
2✔
926

927
        target := pod.Name
2✔
928

2✔
929
        // If this pod is a VM launcher pod, we need to retrieve the name of the VM. This is necessary
2✔
930
        // because we do not use the same syntax for the LSP of normal pods and for VM pods
2✔
931
        if vmName, exists := pod.Annotations[fmt.Sprintf(util.VMAnnotationTemplate, provider)]; exists {
4✔
932
                target = vmName
2✔
933
        }
2✔
934

935
        return ovs.PodNameToPortName(target, pod.Namespace, provider)
2✔
936
}
937

938
// getEndpointTargetLSP returns the name of the LSP on which addresses are attached for a specific pod
939
func (c *Controller) getEndpointTargetLSPName(pod *v1.Pod, addresses []string) (string, error) {
×
940
        // Retrieve the provider for those addresses
×
941
        provider, err := c.getEndpointProvider(pod, addresses)
×
942
        if err != nil {
×
943
                return "", err
×
944
        }
×
945

946
        return getEndpointTargetLSPNameFromProvider(pod, provider), nil
×
947
}
948

949
// getSubnetByProvider returns the subnet linked to a provider on a pod
950
func getSubnetByProvider(pod *v1.Pod, provider string) (string, error) {
×
951
        subnetName, exists := pod.Annotations[fmt.Sprintf(util.LogicalSwitchAnnotationTemplate, provider)]
×
952
        if !exists {
×
953
                return "", fmt.Errorf("couldn't find subnet linked to provider %s", provider)
×
954
        }
×
955

956
        return subnetName, nil
×
957
}
958

959
// getVpcByProvider returns the VPC linked to a provider on a pod.
960
// For underlay subnets without LogicalGateway or U2OInterconnection,
961
// the logical_router annotation is not set, so an empty string is returned.
962
func getVpcByProvider(pod *v1.Pod, provider string) string {
×
963
        return pod.Annotations[fmt.Sprintf(util.LogicalRouterAnnotationTemplate, provider)]
×
964
}
×
965

966
// getEndpointVpcAndSubnet returns the VPC/subnet for a pod and a set of addresses attached to it
967
func (c *Controller) getEndpointVpcAndSubnet(pod *v1.Pod, addresses []string) (string, string, error) {
×
968
        // Retrieve the provider for those addresses
×
969
        provider, err := c.getEndpointProvider(pod, addresses)
×
970
        if err != nil {
×
971
                return "", "", err
×
972
        }
×
973

974
        if provider == "" {
×
975
                return "", "", nil
×
976
        }
×
977

978
        // Retrieve the subnet
979
        subnet, err := getSubnetByProvider(pod, provider)
×
980
        if err != nil {
×
981
                return "", "", err
×
982
        }
×
983

984
        // Retrieve the VPC
985
        vpc := getVpcByProvider(pod, provider)
×
986

×
987
        return vpc, subnet, nil
×
988
}
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