services: add opt-in support for endpointless Cluster LB services

Signed-off-by: Yannick Wahner <thenabsku@gmail.com>
This commit is contained in:
Yannick Wahner
2026-05-29 10:36:12 +02:00
parent e2a0e815fe
commit 69a1d2baa5
3 changed files with 82 additions and 2 deletions

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net"
"strings"
"sync"
"sync/atomic"
"time"
@@ -65,6 +66,8 @@ func (p *Processor) AddOrModify(svcCtx *servicecontext.Context, event watch.Even
log.Error("updating instance", "err", err)
}
allowReconcileWithoutEndpoints := shouldAllowReconcileWithoutEndpoints(service)
// Find out if we have any local endpoints
// if out endpoint is empty then populate it
// if not, go through the endpoints and see if ours still exists
@@ -115,8 +118,11 @@ func (p *Processor) AddOrModify(svcCtx *servicecontext.Context, event watch.Even
}
}
} else {
// There are no local endpoints
if svcCtx.Signalled.Load() {
if allowReconcileWithoutEndpoints {
// Explicit opt-in for controllers that create LoadBalancer services without endpoints
svcCtx.SignalReadiness()
} else if svcCtx.Signalled.Load() {
// There are no local endpoints
svcCtx.ResetReadiness()
p.worker.clear(svcCtx, lastKnownGoodEndpoint, service)
if p.config.EnableARP && !p.config.EnableServicesElection {
@@ -272,6 +278,14 @@ func (p *Processor) startLeaderElection(svcCtx *servicecontext.Context, service
}
}
func shouldAllowReconcileWithoutEndpoints(service *v1.Service) bool {
if service == nil || service.Spec.ExternalTrafficPolicy != v1.ServiceExternalTrafficPolicyTypeCluster {
return false
}
return strings.EqualFold(service.Annotations[kubevip.AllowReconcileWithoutEndpoints], "true")
}
func hasV6(endpoints []string) bool {
for _, e := range endpoints {
ip := net.ParseIP(e)

View File

@@ -0,0 +1,63 @@
package endpoints
import (
"testing"
"github.com/kube-vip/kube-vip/pkg/kubevip"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestShouldAllowReconcileWithoutEndpoints(t *testing.T) {
tests := []struct {
name string
service *v1.Service
expected bool
}{
{
name: "nil service",
expected: false,
},
{
name: "cluster policy with opt-in annotation",
service: &v1.Service{
Spec: v1.ServiceSpec{ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster},
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{kubevip.AllowReconcileWithoutEndpoints: "true"}},
},
expected: true,
},
{
name: "cluster policy with case-insensitive opt-in",
service: &v1.Service{
Spec: v1.ServiceSpec{ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster},
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{kubevip.AllowReconcileWithoutEndpoints: "TRUE"}},
},
expected: true,
},
{
name: "cluster policy without opt-in",
service: &v1.Service{
Spec: v1.ServiceSpec{ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster},
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{}},
},
expected: false,
},
{
name: "local policy with opt-in",
service: &v1.Service{
Spec: v1.ServiceSpec{ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyTypeLocal},
ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{kubevip.AllowReconcileWithoutEndpoints: "true"}},
},
expected: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
actual := shouldAllowReconcileWithoutEndpoints(test.service)
if actual != test.expected {
t.Fatalf("expected %v, got %v", test.expected, actual)
}
})
}
}

View File

@@ -68,6 +68,9 @@ const (
// Name of the service lease object
ServiceLease = "kube-vip.io/leaseName"
// Allow service reconciliation even when no endpoints are present (Cluster policy only)
AllowReconcileWithoutEndpoints = "kube-vip.io/allow-reconcile-without-endpoints"
// Enable DDNS for the service
ServiceDDNS = "kube-vip.io/ddns"