Change GC label to avoid triggering reconcile when updating timestamp (#380)

This removes the use of timestamp to do GC for orphaned endpoint slices.
Instead we delete any endpoint slice copy that no longer exist by
including the upstream endpoint slice name in a label. This has the same
effect as the timestamp but avoids updating the label value on each
reconcile.

Supersedes #377

<!-- codesmith:footer -->
---
<a
href="https://app.blacksmith.sh/netbirdio/codesmith/kubernetes-operator/pr/380"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-light-v2.svg"><img
alt="View with Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/view-with-codesmith-dark-v2.svg"></picture></a>
<a
href="https://backend.blacksmith.sh/track/enable-autofix?expires=1787313622&installation_model_id=427504&pr_number=380&repository=netbirdio%2Fkubernetes-operator&return_to=https%3A%2F%2Fgithub.com%2Fnetbirdio%2Fkubernetes-operator%2Fpull%2F380&signature=f09d17ae5a5b00dcedbdbb84ceb75de82025ba1bec2039d1f7ebd63f47bf8dd0"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-light.svg"><img
alt="Autofix with Codesmith"
src="https://pr-comments-assets.blacksmith.sh/codesmith/autofix-with-codesmith-dark.svg"></picture></a>
<sup>Need help on this PR? Tag <code>/codesmith</code> with what you
need. Autofix is disabled.</sup>

<!-- codesmith:autofix:disabled -->
<!-- /codesmith:footer -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved cleanup of stale egress endpoint slices without relying on
time-based labels.
* Ensured managed endpoint slices keep stable, deterministic
identification across reconciliations.
  * Reduced unnecessary endpoint-slice replacement during updates.
* **Refactor**
* Reorganized egress port computation to simplify reconciliation logic.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Philip Laine
2026-07-23 16:29:06 +02:00
committed by GitHub
parent f8186877fc
commit 627e388cb3
@@ -9,7 +9,6 @@ import (
"maps" "maps"
"net/netip" "net/netip"
"strings" "strings"
"time"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
discoveryv1 "k8s.io/api/discovery/v1" discoveryv1 "k8s.io/api/discovery/v1"
@@ -34,10 +33,10 @@ import (
) )
const ( const (
ForwarderRouterNameLabel = "netbird.io/forwarder-router-name" ForwarderRouterNameLabel = "netbird.io/forwarder-router-name"
EgressRouterNameLabel = "netbird.io/egress-router-name" EgressRouterNameLabel = "netbird.io/egress-router-name"
EgressRouterNamespaceLabel = "netbird.io/egress-router-namespace" EgressRouterNamespaceLabel = "netbird.io/egress-router-namespace"
LastUpdatedLabel = "netbird.io/last-updated" ForwarderEndpointSliceNameLabel = "netbird.io/forwarder-endpoint-slice-name"
) )
// ForwarderServiceReconciler reconciles a EndpointSlice object // ForwarderServiceReconciler reconciles a EndpointSlice object
@@ -88,16 +87,12 @@ func (r *ForwarderServiceReconciler) Reconcile(ctx context.Context, req ctrl.Req
return ctrl.Result{}, err return ctrl.Result{}, err
} }
// Get services for egress resources. // Copy router endpoint slices to egress services.
egressSvcList := &corev1.ServiceList{} egressSvcList := &corev1.ServiceList{}
err = r.Client.List(ctx, egressSvcList, &client.MatchingLabels{EgressRouterNameLabel: routerName, EgressRouterNamespaceLabel: req.Namespace}) err = r.Client.List(ctx, egressSvcList, &client.MatchingLabels{EgressRouterNameLabel: routerName, EgressRouterNamespaceLabel: req.Namespace})
if err != nil { if err != nil {
return ctrl.Result{}, err return ctrl.Result{}, err
} }
// Copy router endpoint slices to egress services.
lastUpdated := fmt.Sprintf("%d", time.Now().Unix())
for _, egressSvc := range egressSvcList.Items { for _, egressSvc := range egressSvcList.Items {
netEgress := &nbv1alpha1.NetworkEgress{ netEgress := &nbv1alpha1.NetworkEgress{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
@@ -110,29 +105,10 @@ func (r *ForwarderServiceReconciler) Reconcile(ctx context.Context, req ctrl.Req
return ctrl.Result{}, err return ctrl.Result{}, err
} }
targetPorts := []int32{} portACs, err := toPortApplyConfigurations(ruleMgr, egressSvc.Spec.Ports, netEgress.Spec.Target)
for _, port := range egressSvc.Spec.Ports { if err != nil {
dest, err := func() (string, error) { return ctrl.Result{}, err
switch {
case netEgress.Spec.Target.IP != nil:
addr, err := netip.ParseAddr(netEgress.Spec.Target.IP.Address)
if err != nil {
return "", err
}
return netip.AddrPortFrom(addr, uint16(port.Port)).String(), nil
case netEgress.Spec.Target.FQDN != nil:
return fmt.Sprintf("%s:%d", netEgress.Spec.Target.FQDN.Hostname, port.Port), nil
}
return "", errors.New("egress target not found")
}()
if err != nil {
return ctrl.Result{}, err
}
rule := ruleMgr.Allocate(port.Protocol, dest)
targetPorts = append(targetPorts, rule.Port)
} }
portACs := toPortApplyConfigurations(egressSvc.Spec.Ports, targetPorts)
egressSvcOwnerRef, err := k8sutil.ControllerReference(&egressSvc, r.Scheme()) egressSvcOwnerRef, err := k8sutil.ControllerReference(&egressSvc, r.Scheme())
if err != nil { if err != nil {
return ctrl.Result{}, err return ctrl.Result{}, err
@@ -140,12 +116,10 @@ func (r *ForwarderServiceReconciler) Reconcile(ctx context.Context, req ctrl.Req
for _, endpointSlice := range endpointSliceList.Items { for _, endpointSlice := range endpointSliceList.Items {
nameSuffx := strings.TrimPrefix(endpointSlice.Name, endpointSlice.GenerateName) nameSuffx := strings.TrimPrefix(endpointSlice.Name, endpointSlice.GenerateName)
labels := map[string]string{ labels := maps.Clone(egressSvc.Labels)
discoveryv1.LabelServiceName: egressSvc.Name, labels[discoveryv1.LabelManagedBy] = "netbird-operator.netbird.io"
discoveryv1.LabelManagedBy: "netbird-operator.netbird.io", labels[discoveryv1.LabelServiceName] = egressSvc.Name
LastUpdatedLabel: lastUpdated, labels[ForwarderEndpointSliceNameLabel] = endpointSlice.Name
}
maps.Copy(labels, egressSvc.Labels)
endpointACs := toEndpointApplyConfigurations(endpointSlice.Endpoints) endpointACs := toEndpointApplyConfigurations(endpointSlice.Endpoints)
endpointSliceAC := discoveryv1ac.EndpointSlice(fmt.Sprintf("%s-%s", egressSvc.Name, nameSuffx), egressSvc.Namespace). endpointSliceAC := discoveryv1ac.EndpointSlice(fmt.Sprintf("%s-%s", egressSvc.Name, nameSuffx), egressSvc.Namespace).
@@ -175,10 +149,6 @@ func (r *ForwarderServiceReconciler) Reconcile(ctx context.Context, req ctrl.Req
} }
// Cleanup old endpoint slices. // Cleanup old endpoint slices.
updateReq, err := labels.NewRequirement(LastUpdatedLabel, selection.NotEquals, []string{lastUpdated})
if err != nil {
return ctrl.Result{}, err
}
egressNameReq, err := labels.NewRequirement(EgressRouterNameLabel, selection.Equals, []string{routerName}) egressNameReq, err := labels.NewRequirement(EgressRouterNameLabel, selection.Equals, []string{routerName})
if err != nil { if err != nil {
return ctrl.Result{}, err return ctrl.Result{}, err
@@ -191,8 +161,18 @@ func (r *ForwarderServiceReconciler) Reconcile(ctx context.Context, req ctrl.Req
if err != nil { if err != nil {
return ctrl.Result{}, err return ctrl.Result{}, err
} }
deleteSelector := labels.NewSelector().Add(*updateReq).Add(*egressNameReq).Add(*egressNamespaceReq).Add(*sourceReq) deleteSelector := labels.NewSelector().Add(*egressNameReq).Add(*egressNamespaceReq).Add(*sourceReq)
if len(endpointSliceList.Items) > 0 {
endpointSliceNames := []string{}
for _, endpointSlice := range endpointSliceList.Items {
endpointSliceNames = append(endpointSliceNames, endpointSlice.Name)
}
endPointSliceReq, err := labels.NewRequirement(ForwarderEndpointSliceNameLabel, selection.NotIn, endpointSliceNames)
if err != nil {
return ctrl.Result{}, err
}
deleteSelector = deleteSelector.Add(*endPointSliceReq)
}
err = r.Client.List(ctx, endpointSliceList, client.MatchingLabelsSelector{Selector: deleteSelector}) err = r.Client.List(ctx, endpointSliceList, client.MatchingLabelsSelector{Selector: deleteSelector})
if err != nil { if err != nil {
return ctrl.Result{}, err return ctrl.Result{}, err
@@ -331,14 +311,29 @@ func toEndpointApplyConfigurations(endpoints []discoveryv1.Endpoint) []*discover
return endpointACs return endpointACs
} }
func toPortApplyConfigurations(ports []corev1.ServicePort, targetPorts []int32) []*discoveryv1ac.EndpointPortApplyConfiguration { func toPortApplyConfigurations(ruleMgr *forwarder.RuleManager, ports []corev1.ServicePort, target nbv1alpha1.NetworkEgressTarget) ([]*discoveryv1ac.EndpointPortApplyConfiguration, error) {
portACs := make([]*discoveryv1ac.EndpointPortApplyConfiguration, 0, len(ports)) portACs := make([]*discoveryv1ac.EndpointPortApplyConfiguration, 0, len(ports))
for i, port := range ports { for _, port := range ports {
portAC := discoveryv1ac.EndpointPort().WithName(port.Name).WithPort(targetPorts[i]).WithProtocol(port.Protocol) dest := ""
switch {
case target.IP != nil:
addr, err := netip.ParseAddr(target.IP.Address)
if err != nil {
return nil, err
}
dest = netip.AddrPortFrom(addr, uint16(port.Port)).String()
case target.FQDN != nil:
dest = fmt.Sprintf("%s:%d", target.FQDN.Hostname, port.Port)
default:
return nil, errors.New("egress target not found")
}
rule := ruleMgr.Allocate(port.Protocol, dest)
portAC := discoveryv1ac.EndpointPort().WithName(port.Name).WithPort(rule.Port).WithProtocol(port.Protocol)
if port.AppProtocol != nil { if port.AppProtocol != nil {
portAC = portAC.WithAppProtocol(*port.AppProtocol) portAC = portAC.WithAppProtocol(*port.AppProtocol)
} }
portACs = append(portACs, portAC) portACs = append(portACs, portAC)
} }
return portACs return portACs, nil
} }