Repository: metallb/metallb
Pull request #2777 (2025-06-01 23:02:46)

bump golangci-lint to v2
**Is this a BUG FIX or a FEATURE ?**:

/kind cleanup

**What this PR does / why we need it**:
This bumps `golangci-lint` to the latest v2 release to take advantage of updated linters and converts the config file to use the new config format. Addresses any lints that come along with latest release(s)

**Special notes for your reviewer**:
N/A
**Release note**:
<!--  Write your release note:

```release-note
NONE
```



Patch:
diff --git a/.golangci.yml b/.golangci.yml
index fabfe013..56f7c55e 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -1,54 +1,72 @@
+version: "2"
 run:
   issues-exit-code: 2
 linters:
   enable:
-  - godot
-  - goimports
-  - gofmt
-  - ginkgolinter
-  - dogsled
-  - copyloopvar
-  - gocritic
-  - misspell
-  - nolintlint
-  - stylecheck
-  - unconvert
-  - unparam
-  - whitespace
-  - revive
-  - unused
-  - wastedassign
-linters-settings:
-  errcheck:
-    exclude-functions:
-      - (github.com/go-kit/log.Logger).Log
-  godot:
-    scope: toplevel
-    capital: true
-    exclude:
-    - 'SPDX-License-Identifier.*'
-    - '\+groupName.*'
-  misspell:
-    locale: US
-    ignore-words:
-      - neighbours
-      - neighbour
-  stylecheck:
-    dot-import-whitelist:
-    - github.com/onsi/ginkgo
-    - github.com/onsi/gomega
-  gocritic:
-    disabled-checks:
-      - captLocal
-      - exitAfterDefer
-  revive:
+    - copyloopvar
+    - dogsled
+    - ginkgolinter
+    - gocritic
+    - godot
+    - misspell
+    - nolintlint
+    - revive
+    - staticcheck
+    - unconvert
+    - unparam
+    - wastedassign
+    - whitespace
+  settings:
+    errcheck:
+      exclude-functions:
+        - (github.com/go-kit/log.Logger).Log
+    gocritic:
+      disabled-checks:
+        - captLocal
+        - exitAfterDefer
+    godot:
+      scope: toplevel
+      exclude:
+        - SPDX-License-Identifier.*
+        - \+groupName.*
+      capital: true
+    misspell:
+      locale: US
+      ignore-rules:
+        - neighbours
+        - neighbour
+    revive:
+      rules:
+        - name: receiver-naming
+          disabled: true
+        - name: dot-imports
+          disabled: true
+    staticcheck:
+      dot-import-whitelist:
+        - github.com/onsi/ginkgo
+        - github.com/onsi/gomega
+  exclusions:
+    generated: lax
+    presets:
+      - comments
+      - common-false-positives
+      - legacy
+      - std-error-handling
     rules:
-    - name: receiver-naming
-      disabled: true
-    - name: dot-imports
-      disabled: true
-issues:
-  exclude-rules:
-    - path: 'api'
-      linters:
-        - stylecheck
+      - linters:
+          - staticcheck
+        path: api
+    paths:
+      - third_party$
+      - builtin$
+      - examples$
+formatters:
+  enable:
+    - gofmt
+    - goimports
+  exclusions:
+    generated: lax
+    paths:
+      - third_party$
+      - builtin$
+      - examples$
diff --git a/internal/allocator/allocator.go b/internal/allocator/allocator.go
index 5358cd48..5025a19a 100644
--- a/internal/allocator/allocator.go
+++ b/internal/allocator/allocator.go
@@ -562,7 +562,7 @@ func (a *Allocator) IPs(svc string) []net.IP {
 
 func (a *Allocator) AllocationKey(svc string) string {
 	if alloc := a.allocated[svc]; alloc != nil {
-		return alloc.key.backend + alloc.key.sharing
+		return alloc.backend + alloc.sharing
 	}
 	return ""
 }
diff --git a/internal/config/config.go b/internal/config/config.go
index ed4c5c9c..04557e22 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -32,7 +32,6 @@ import (
 	metallbv1beta2 "go.universe.tf/metallb/api/v1beta2"
 	"go.universe.tf/metallb/internal/bgp/community"
 	"go.universe.tf/metallb/internal/ipfamily"
-	k8snodes "go.universe.tf/metallb/internal/k8s/nodes"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
@@ -328,13 +327,6 @@ func poolsFor(resources ClusterResources) (*Pools, error) {
 					return nil, fmt.Errorf("CIDR %q in pool %q overlaps with already defined CIDR %q", cidr, p.Name, m)
 				}
 			}
-			// Check pool CIDR is not overlapping with Node IPs
-			nodeIps := k8snodes.NodeIPsForFamily(resources.Nodes, ipfamily.ForCIDR(cidr))
-			for _, nodeIP := range nodeIps {
-				if cidr.Contains(nodeIP) {
-					return nil, fmt.Errorf("pool cidr %q contains nodeIp %q", cidr, nodeIP)
-				}
-			}
 			allCIDRs = append(allCIDRs, cidr)
 		}
 
diff --git a/internal/k8s/controllers/bgp_status_controller.go b/internal/k8s/controllers/bgp_status_controller.go
index 14594952..a6e0fe3c 100644
--- a/internal/k8s/controllers/bgp_status_controller.go
+++ b/internal/k8s/controllers/bgp_status_controller.go
@@ -59,27 +59,27 @@ type ServiceBGPStatusReconciler struct {
 }
 
 func (r *ServiceBGPStatusReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	level.Info(r.Logger).Log("controller", "ServiceBGPStatus", "start reconcile", req.NamespacedName.String())
-	defer level.Info(r.Logger).Log("controller", "ServiceBGPStatus", "end reconcile", req.NamespacedName.String())
+	level.Info(r.Logger).Log("controller", "ServiceBGPStatus", "start reconcile", req.String())
+	defer level.Info(r.Logger).Log("controller", "ServiceBGPStatus", "end reconcile", req.String())
 
 	serviceName, serviceNamespace := req.Name, req.Namespace
 
 	var serviceBGPStatuses v1beta1.ServiceBGPStatusList
-	err := r.Client.List(ctx, &serviceBGPStatuses, client.MatchingFields{
+	err := r.List(ctx, &serviceBGPStatuses, client.MatchingFields{
 		serviceIndexName: indexFor(serviceNamespace, serviceName, r.NodeName),
 	})
 	if err != nil {
 		return ctrl.Result{}, err
 	}
 
-	peers := r.PeersFetcher(req.NamespacedName.String())
+	peers := r.PeersFetcher(req.String())
 	if peers.Len() == 0 {
 		errs := []error{}
 		for i := range serviceBGPStatuses.Items {
 			if serviceBGPStatuses.Items[i].Labels[LabelAnnounceNode] != r.NodeName { // shouldn't happen because of the indexing, just in case
 				continue
 			}
-			if err := r.Client.Delete(ctx, &serviceBGPStatuses.Items[i]); err != nil && !apierrors.IsNotFound(err) {
+			if err := r.Delete(ctx, &serviceBGPStatuses.Items[i]); err != nil && !apierrors.IsNotFound(err) {
 				errs = append(errs, err)
 			}
 		}
@@ -94,7 +94,7 @@ func (r *ServiceBGPStatusReconciler) Reconcile(ctx context.Context, req ctrl.Req
 			if serviceBGPStatuses.Items[i+1].Labels[LabelAnnounceNode] != r.NodeName {
 				continue
 			}
-			if err := r.Client.Delete(ctx, &serviceBGPStatuses.Items[i+1]); err != nil && !apierrors.IsNotFound(err) {
+			if err := r.Delete(ctx, &serviceBGPStatuses.Items[i+1]); err != nil && !apierrors.IsNotFound(err) {
 				deleteRedundantErrs = append(deleteRedundantErrs, err)
 			}
 		}
diff --git a/internal/k8s/controllers/config_controller.go b/internal/k8s/controllers/config_controller.go
index 4f741273..1e4d203e 100644
--- a/internal/k8s/controllers/config_controller.go
+++ b/internal/k8s/controllers/config_controller.go
@@ -55,8 +55,8 @@ func (r *ConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 }
 
 var requestHandler = func(r *ConfigReconciler, ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	level.Info(r.Logger).Log("controller", "ConfigReconciler", "start reconcile", req.NamespacedName.String())
-	defer level.Info(r.Logger).Log("controller", "ConfigReconciler", "end reconcile", req.NamespacedName.String())
+	level.Info(r.Logger).Log("controller", "ConfigReconciler", "start reconcile", req.String())
+	defer level.Info(r.Logger).Log("controller", "ConfigReconciler", "end reconcile", req.String())
 	updates.Inc()
 
 	var ipAddressPools metallbv1beta1.IPAddressPoolList
diff --git a/internal/k8s/controllers/frrk8s_config_controller.go b/internal/k8s/controllers/frrk8s_config_controller.go
index e9ebe84a..762198a1 100644
--- a/internal/k8s/controllers/frrk8s_config_controller.go
+++ b/internal/k8s/controllers/frrk8s_config_controller.go
@@ -75,8 +75,8 @@ type FRRK8sReconciler struct {
 }
 
 func (r *FRRK8sReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	level.Info(r.Logger).Log("controller", "FRRK8sReconciler", "start reconcile", req.NamespacedName.String())
-	defer level.Info(r.Logger).Log("controller", "FRRK8sReconciler", "end reconcile", req.NamespacedName.String())
+	level.Info(r.Logger).Log("controller", "FRRK8sReconciler", "start reconcile", req.String())
+	defer level.Info(r.Logger).Log("controller", "FRRK8sReconciler", "end reconcile", req.String())
 	updates.Inc()
 
 	r.Lock()
diff --git a/internal/k8s/controllers/layer2_status_controller.go b/internal/k8s/controllers/layer2_status_controller.go
index 1a53b7d3..5acb7a40 100644
--- a/internal/k8s/controllers/layer2_status_controller.go
+++ b/internal/k8s/controllers/layer2_status_controller.go
@@ -65,14 +65,14 @@ type Layer2StatusReconciler struct {
 }
 
 func (r *Layer2StatusReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	level.Info(r.Logger).Log("controller", "Layer2StatusReconciler", "start reconcile", req.NamespacedName.String())
-	defer level.Info(r.Logger).Log("controller", "Layer2StatusReconciler", "end reconcile", req.NamespacedName.String())
+	level.Info(r.Logger).Log("controller", "Layer2StatusReconciler", "start reconcile", req.String())
+	defer level.Info(r.Logger).Log("controller", "Layer2StatusReconciler", "end reconcile", req.String())
 
 	serviceName, serviceNamespace := req.Name, req.Namespace
 
 	ipAdvS := r.StatusFetcher(types.NamespacedName{Name: serviceName, Namespace: serviceNamespace})
 	var serviceL2statuses v1beta1.ServiceL2StatusList
-	if err := r.Client.List(ctx, &serviceL2statuses, client.MatchingFields{
+	if err := r.List(ctx, &serviceL2statuses, client.MatchingFields{
 		serviceIndexName: types.NamespacedName{Name: serviceName, Namespace: serviceNamespace}.String(),
 	}); err != nil {
 		return ctrl.Result{}, err
@@ -84,7 +84,7 @@ func (r *Layer2StatusReconciler) Reconcile(ctx context.Context, req ctrl.Request
 			if item.Labels[LabelAnnounceNode] != r.NodeName {
 				continue
 			}
-			if err := r.Client.Delete(ctx, &serviceL2statuses.Items[key]); err != nil && !errors.IsNotFound(err) {
+			if err := r.Delete(ctx, &serviceL2statuses.Items[key]); err != nil && !errors.IsNotFound(err) {
 				errs = append(errs, err)
 			}
 		}
@@ -98,7 +98,7 @@ func (r *Layer2StatusReconciler) Reconcile(ctx context.Context, req ctrl.Request
 			if serviceL2statuses.Items[i+1].Labels[LabelAnnounceNode] != r.NodeName {
 				continue
 			}
-			if err := r.Client.Delete(ctx, &serviceL2statuses.Items[i+1]); err != nil && !errors.IsNotFound(err) {
+			if err := r.Delete(ctx, &serviceL2statuses.Items[i+1]); err != nil && !errors.IsNotFound(err) {
 				deleteRedundantErrs = append(deleteRedundantErrs, err)
 			}
 		}
diff --git a/internal/k8s/controllers/node_controller.go b/internal/k8s/controllers/node_controller.go
index dc8a9790..d99d8d29 100644
--- a/internal/k8s/controllers/node_controller.go
+++ b/internal/k8s/controllers/node_controller.go
@@ -42,8 +42,8 @@ type NodeReconciler struct {
 }
 
 func (r *NodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	level.Info(r.Logger).Log("controller", "NodeReconciler", "start reconcile", req.NamespacedName.String())
-	defer level.Info(r.Logger).Log("controller", "NodeReconciler", "end reconcile", req.NamespacedName.String())
+	level.Info(r.Logger).Log("controller", "NodeReconciler", "start reconcile", req.String())
+	defer level.Info(r.Logger).Log("controller", "NodeReconciler", "end reconcile", req.String())
 	updates.Inc()
 
 	var n corev1.Node
diff --git a/internal/k8s/controllers/pool_controller.go b/internal/k8s/controllers/pool_controller.go
index d1041163..b96865a0 100644
--- a/internal/k8s/controllers/pool_controller.go
+++ b/internal/k8s/controllers/pool_controller.go
@@ -45,8 +45,8 @@ type PoolReconciler struct {
 }
 
 func (r *PoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	level.Info(r.Logger).Log("controller", "PoolReconciler", "start reconcile", req.NamespacedName.String())
-	defer level.Info(r.Logger).Log("controller", "PoolReconciler", "end reconcile", req.NamespacedName.String())
+	level.Info(r.Logger).Log("controller", "PoolReconciler", "start reconcile", req.String())
+	defer level.Info(r.Logger).Log("controller", "PoolReconciler", "end reconcile", req.String())
 	updates.Inc()
 
 	var ipAddressPools metallbv1beta1.IPAddressPoolList
diff --git a/internal/k8s/controllers/pool_status_controller.go b/internal/k8s/controllers/pool_status_controller.go
index 822ebb24..a2692fc8 100644
--- a/internal/k8s/controllers/pool_status_controller.go
+++ b/internal/k8s/controllers/pool_status_controller.go
@@ -50,8 +50,8 @@ type PoolStatusReconciler struct {
 }
 
 func (r *PoolStatusReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	level.Info(r.Logger).Log("controller", "PoolStatusReconciler", "start reconcile", req.NamespacedName.String())
-	defer level.Info(r.Logger).Log("controller", "PoolStatusReconciler", "end reconcile", req.NamespacedName.String())
+	level.Info(r.Logger).Log("controller", "PoolStatusReconciler", "start reconcile", req.String())
+	defer level.Info(r.Logger).Log("controller", "PoolStatusReconciler", "end reconcile", req.String())
 
 	var pool v1beta1.IPAddressPool
 	err := r.Get(ctx, req.NamespacedName, &pool)
diff --git a/internal/k8s/controllers/service_controller.go b/internal/k8s/controllers/service_controller.go
index a804ee76..dd178135 100644
--- a/internal/k8s/controllers/service_controller.go
+++ b/internal/k8s/controllers/service_controller.go
@@ -61,8 +61,8 @@ func (r *ServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct
 }
 
 func (r *ServiceReconciler) reconcileService(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	level.Info(r.Logger).Log("controller", "ServiceReconciler", "start reconcile", req.NamespacedName.String())
-	defer level.Info(r.Logger).Log("controller", "ServiceReconciler", "end reconcile", req.NamespacedName.String())
+	level.Info(r.Logger).Log("controller", "ServiceReconciler", "start reconcile", req.String())
+	defer level.Info(r.Logger).Log("controller", "ServiceReconciler", "end reconcile", req.String())
 	updates.Inc()
 
 	var service *v1.Service
@@ -94,14 +94,14 @@ func (r *ServiceReconciler) reconcileService(ctx context.Context, req ctrl.Reque
 	if service != nil {
 		level.Debug(r.Logger).Log("controller", "ServiceReconciler", "processing service", dumpResource(service))
 	} else {
-		level.Debug(r.Logger).Log("controller", "ServiceReconciler", "processing deletion on service", req.NamespacedName.String())
+		level.Debug(r.Logger).Log("controller", "ServiceReconciler", "processing deletion on service", req.String())
 	}
 
-	res := r.Handler(r.Logger, req.NamespacedName.String(), service, epSlices)
+	res := r.Handler(r.Logger, req.String(), service, epSlices)
 	switch res {
 	case SyncStateError:
 		updateErrors.Inc()
-		level.Info(r.Logger).Log("controller", "ServiceReconciler", "name", req.NamespacedName.String(), "service", dumpResource(service), "endpoints", dumpResource(epSlices), "event", "failed to handle service")
+		level.Info(r.Logger).Log("controller", "ServiceReconciler", "name", req.String(), "service", dumpResource(service), "endpoints", dumpResource(epSlices), "event", "failed to handle service")
 		return ctrl.Result{}, errRetry
 	case SyncStateReprocessAll:
 		level.Info(r.Logger).Log("controller", "ServiceReconciler", "event", "force service reload")
@@ -109,7 +109,7 @@ func (r *ServiceReconciler) reconcileService(ctx context.Context, req ctrl.Reque
 		return ctrl.Result{}, nil
 	case SyncStateErrorNoRetry:
 		updateErrors.Inc()
-		level.Error(r.Logger).Log("controller", "ServiceReconciler", "name", req.NamespacedName.String(), "service", dumpResource(service), "endpoints", dumpResource(epSlices), "event", "failed to handle service")
+		level.Error(r.Logger).Log("controller", "ServiceReconciler", "name", req.String(), "service", dumpResource(service), "endpoints", dumpResource(epSlices), "event", "failed to handle service")
 		return ctrl.Result{}, nil
 	}
 	return ctrl.Result{}, nil
diff --git a/internal/k8s/controllers/service_controller_reload.go b/internal/k8s/controllers/service_controller_reload.go
index 460d5f14..6da82de4 100644
--- a/internal/k8s/controllers/service_controller_reload.go
+++ b/internal/k8s/controllers/service_controller_reload.go
@@ -60,8 +60,8 @@ func isReloadReq(req ctrl.Request) bool {
 }
 
 func (r *ServiceReconciler) reprocessAll(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-	level.Info(r.Logger).Log("controller", "ServiceReconciler - reprocessAll", "start reconcile", req.NamespacedName.String())
-	defer level.Info(r.Logger).Log("controller", "ServiceReconciler - reprocessAll", "end reconcile", req.NamespacedName.String())
+	level.Info(r.Logger).Log("controller", "ServiceReconciler - reprocessAll", "start reconcile", req.String())
+	defer level.Info(r.Logger).Log("controller", "ServiceReconciler - reprocessAll", "end reconcile", req.String())
 
 	var services v1.ServiceList
 	if err := r.List(ctx, &services); err != nil {
diff --git a/internal/k8s/nodes/nodes.go b/internal/k8s/nodes/nodes.go
index 6c7e8ad3..88986098 100644
--- a/internal/k8s/nodes/nodes.go
+++ b/internal/k8s/nodes/nodes.go
@@ -3,9 +3,6 @@
 package nodes
 
 import (
-	"net"
-
-	"go.universe.tf/metallb/internal/ipfamily"
 	corev1 "k8s.io/api/core/v1"
 )
 
@@ -40,20 +37,3 @@ func IsNodeExcludedFromBalancers(n *corev1.Node) bool {
 	}
 	return false
 }
-
-// NodeIPsForFamily returns all input node nodeIP based on ipfamily.
-func NodeIPsForFamily(nodes []corev1.Node, family ipfamily.Family) []net.IP {
-	var nodeIPs []net.IP
-	for _, n := range nodes {
-		for _, a := range n.Status.Addresses {
-			if a.Type == corev1.NodeInternalIP {
-				nodeIP := net.ParseIP(a.Address)
-				if family != ipfamily.DualStack && ipfamily.ForAddress(nodeIP) != family {
-					continue
-				}
-				nodeIPs = append(nodeIPs, nodeIP)
-			}
-		}
-	}
-	return nodeIPs
-}
diff --git a/internal/k8s/webhooks/webhookv1beta1/ipaddresspool_webhook.go b/internal/k8s/webhooks/webhookv1beta1/ipaddresspool_webhook.go
index 3f30ae95..ec797034 100644
--- a/internal/k8s/webhooks/webhookv1beta1/ipaddresspool_webhook.go
+++ b/internal/k8s/webhooks/webhookv1beta1/ipaddresspool_webhook.go
@@ -104,13 +104,8 @@ func validateIPAddressPoolCreate(ipAddress *v1beta1.IPAddressPool) error {
 		return err
 	}
 
-	nodes, err := getExistingNodes()
-	if err != nil {
-		return err
-	}
-
 	toValidate := ipAddressListWithUpdate(existingIPAddressPoolList, ipAddress)
-	err = Validator.Validate(toValidate, nodes)
+	err = Validator.Validate(toValidate)
 	if err != nil {
 		level.Error(Logger).Log("webhook", "ipAddress", "action", "create", "name", ipAddress.Name, "namespace", ipAddress.Namespace, "error", err)
 		return err
@@ -127,13 +122,8 @@ func validateIPAddressPoolUpdate(ipAddress *v1beta1.IPAddressPool, _ *v1beta1.IP
 		return err
 	}
 
-	nodes, err := getExistingNodes()
-	if err != nil {
-		return err
-	}
-
 	toValidate := ipAddressListWithUpdate(existingIPAddressPoolList, ipAddress)
-	err = Validator.Validate(toValidate, nodes)
+	err = Validator.Validate(toValidate)
 	if err != nil {
 		level.Error(Logger).Log("webhook", "ipAddress", "action", "update", "name", ipAddress.Name, "namespace", ipAddress.Namespace, "error", err)
 		return err
diff --git a/speaker/main.go b/speaker/main.go
index 68b8b66e..005c4a49 100644
--- a/speaker/main.go
+++ b/speaker/main.go
@@ -197,10 +197,11 @@ func main() {
 		validateConfig = config.DiscardNativeOnly
 	}
 
-	listenFRRK8s := false
-	if bgpType == string(bgpFrrK8s) {
-		listenFRRK8s = true
-	}
+	// listenFRRK8s := false
+	listenFRRK8s := bgpType == string(bgpFrrK8s)
+	// if bgpType == string(bgpFrrK8s) {
+	// 	listenFRRK8s = true
+	// }
 	client, err := k8s.New(&k8s.Config{
 		ProcessName: "metallb-speaker",
 		NodeName:    *myNode,
diff --git a/tasks.py b/tasks.py
index 6f2f9455..14ebc813 100644
--- a/tasks.py
+++ b/tasks.py
@@ -1130,7 +1130,7 @@ def lint(ctx, env="container"):
     convenient to install the golangci-lint binaries on the host. This can be
     achieved by running `inv lint --env host`.
     """
-    version = "1.64.7"
+    version = "2.1.6"
     golangci_cmd = "golangci-lint run --timeout 10m0s ./..."
 
     if env == "container":
diff --git a/website/content/release-notes/_index.md b/website/content/release-notes/_index.md
index cde48dde..4f7d73fb 100644
--- a/website/content/release-notes/_index.md
+++ b/website/content/release-notes/_index.md
@@ -2,6 +2,52 @@
 title: Release Notes
 weight: 8
 ---
+## Version 0.15.2
+
+### Bugs or Regressions
+
+- Publish the chart to quay as too (#2782, @fedepaol)
+
+## Version 0.15.1
+
+### Bugs or Regressions
+
+- Revert refusing ips overlapping with node cidrs as ip ranges are not handled correctly. (#2780, @fedepaol)
+
+## Version 0.15.0
+
+### Features
+
+- Add IPAddressPool Status, exposing the available/assignment counters for the pool. (#2643, @oribon)
+- Add the ServiceBGPStatus resource, allowing a simple way to understand which service is advertised to which peers via BGP. (#2650, @oribon)
+- Add unnumbered BGP peering for frrk8s backend (#2605, @karampok)
+- Deprecate the disableMP flag, and make the v4 over v4 and v6 over v6 behavior the default. Additionally, add a dualStackFamily flag per neighbor, where we enable the v4 family over a v6 link and viceversa. (#2766, @fedepaol)
+- Helm: Add option to exclude pools from triggering the relevant prometheus alerts (#2761, @oribon)
+- Publish charts to quay, along side existing publishing methods. (#2647, @a1994sc)
+- Support Unnumbered BGP for frr backend (#2673, @karampok)
+
+### Documentation
+
+- Docs: clarify how to install on Openshift via Helm. (#2694, @fedepaol)
+- Docs: replace deprecated metallb.universe.tf annotation prefix with metallb.io (#2639, @tamcore)
+
+### Bugs or Regressions
+
+- Allow different local preferences if referring to different aggregation lengths. The previous check was too restrictive and was checking that both aggregation length were empty (even with v4 only addresses). (#2693, @fedepaol)
+- Bug Fix : When both namespace and service selectors are absent in ServiceAllocation spec of IPAddressPool, we now default to selecting everything using `labels.Everything()` for ServiceSelectors. (#2714, @ashikjm)
+- Don't allow IPAddressPools providing IPs overlapping with node IPs. (#2585, @tyiying)
+- Even if the service is prefer dual stack and dual stack pools are available, provide loadbalancer ips that match address family (single, dual) of the service's cluster ip. (#2726, @fedepaol)
+- Fix the ServiceL2Status controller attempting to overwrite resources that do not belong to its node. (#2712, @oribon)
+- Layer2 mode does not work when memberlist is disabled. Here we restore the original operating mode that assumes all the speakers are running on all the nodes if memberlist is disabled. (#2549, @karampok)
+- Limit the frr processes' file descriptors limit to 100000 instead of defaulting to host's ulimit value. (#2725, @fedepaol)
+- Remove valid, but expired "placeholder" caBundle from the BGPPeers CRD conversion webhook configuration. (#2696, @dlipovetsky)
+- Reprocess the services and reassign IPs if the sharing criteria of a given service changes (ie, the external traffic policy changes and suddenly another service in pending becomes compatible). (#2692, @fedepaol)
+- Revert `Remove advertisements from peers when local node is cordoned` behavior (#2715, @karampok)
+
+This release includes contributions from:
+
+Adriano Pezzuto, Allen Conlon, Ashik J M, Christopher Dziomba, Daniel Lipovetsky, Eduardo Minguez, Federico Paolinelli, Konstantinos Karampogias, Ori Braunshtein, Philipp Born, Raoul Scarazzini, Roman Dodin, Stephan, Tyler Auerbeck, Yi-Ying Tan
+
 
 ## Version 0.14.9
 


Test patch:
diff --git a/controller/controller_test.go b/controller/controller_test.go
index 0cd9a7ad..5ad23bf0 100644
--- a/controller/controller_test.go
+++ b/controller/controller_test.go
@@ -29,13 +29,13 @@ func diffService(a, b *v1.Service) string {
 	if a != nil {
 		newA := new(v1.Service)
 		*newA = *a
-		newA.ObjectMeta.DeletionTimestamp = &metav1.Time{}
+		newA.DeletionTimestamp = &metav1.Time{}
 		a = newA
 	}
 	if b != nil {
 		newB := new(v1.Service)
 		*newB = *b
-		newB.ObjectMeta.DeletionTimestamp = &metav1.Time{}
+		newB.DeletionTimestamp = &metav1.Time{}
 		b = newB
 	}
 	return cmp.Diff(a, b)
diff --git a/e2etest/pkg/frr/bgp.go b/e2etest/pkg/frr/bgp.go
index 93188e68..9c23a5de 100644
--- a/e2etest/pkg/frr/bgp.go
+++ b/e2etest/pkg/frr/bgp.go
@@ -219,12 +219,20 @@ func RawDump(exec executor.Executor, filesToDump ...string) (string, error) {
 	}
 
 	res += "####### Network setup for host\n"
+	res += "####### Routes\n"
 	out, err = exec.Exec("bash", "-c", "ip -6 route; ip -4 route")
 	if err != nil {
 		allerrs = errors.Join(allerrs, fmt.Errorf("\nFailed exec to print network setup: %v", err))
 	}
 	res += out
 
+	res += "####### Interfaces\n"
+	out, err = exec.Exec("bash", "-c", "ip address show; ip link show")
+	if err != nil {
+		allerrs = errors.Join(allerrs, fmt.Errorf("\nFailed exec to print network setup: %v", err))
+	}
+	res += out
+
 	if allerrs.Error() == "" {
 		allerrs = nil
 	}
diff --git a/e2etest/webhookstests/webhooks.go b/e2etest/webhookstests/webhooks.go
index 3e391973..166788c3 100644
--- a/e2etest/webhookstests/webhooks.go
+++ b/e2etest/webhookstests/webhooks.go
@@ -21,7 +21,6 @@ package webhookstests
 import (
 	"context"
 	"fmt"
-	"net"
 
 	"go.universe.tf/e2etest/pkg/config"
 	"go.universe.tf/e2etest/pkg/k8s"
@@ -31,12 +30,9 @@ import (
 	. "github.com/onsi/gomega"
 	"github.com/openshift-kni/k8sreporter"
 
-	"go.universe.tf/e2etest/pkg/ipfamily"
-	"go.universe.tf/e2etest/pkg/k8sclient"
 	metallbv1beta1 "go.universe.tf/metallb/api/v1beta1"
 	metallbv1beta2 "go.universe.tf/metallb/api/v1beta2"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	clientset "k8s.io/client-go/kubernetes"
 	"k8s.io/utils/ptr"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
@@ -48,10 +44,7 @@ var (
 )
 
 var _ = ginkgo.Describe("Webhooks", func() {
-	var cs clientset.Interface
 	ginkgo.BeforeEach(func() {
-		cs = k8sclient.New()
-
 		ginkgo.By("Clearing any previous configuration")
 		err := ConfigUpdater.Clean()
 		Expect(err).NotTo(HaveOccurred())
@@ -113,45 +106,6 @@ var _ = ginkgo.Describe("Webhooks", func() {
 			Expect(err).To(HaveOccurred())
 			Expect(err.Error()).To(ContainSubstring("overlaps with already defined CIDR"))
 		})
-
-		ginkgo.DescribeTable("IPAddressPool with overlapping addresses of the nodes",
-			func(ipFamily ipfamily.Family) {
-				nodes, err := cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
-				Expect(err).NotTo(HaveOccurred())
-				nodeIps, err := k8s.NodeIPsForFamily(nodes.Items, ipFamily, "")
-				Expect(err).NotTo(HaveOccurred())
-				Expect(len(nodeIps)).To(BeNumerically(">", 0), "empty node ips list")
-				nodeIP := net.ParseIP(nodeIps[0])
-				cidr := &net.IPNet{
-					IP:   nodeIP,
-					Mask: net.CIDRMask(32, 32),
-				}
-				if ipFamily == ipfamily.IPv6 {
-					cidr.Mask = net.CIDRMask(128, 128)
-				}
-
-				ginkgo.By("Creating IPAddressPool")
-				var nodeCIDRs []string
-				nodeCIDRs = append(nodeCIDRs, cidr.String())
-				resources := config.Resources{
-					Pools: []metallbv1beta1.IPAddressPool{
-						{
-							ObjectMeta: metav1.ObjectMeta{
-								Name: "webhooks-test3",
-							},
-							Spec: metallbv1beta1.IPAddressPoolSpec{
-								Addresses: nodeCIDRs,
-							},
-						},
-					},
-				}
-				err = ConfigUpdater.Update(resources)
-				Expect(err).To(HaveOccurred())
-				Expect(err.Error()).To(ContainSubstring("contains nodeIp"))
-			},
-			ginkgo.Entry("IPV4", ipfamily.IPv4),
-			ginkgo.Entry("IPV6", ipfamily.IPv6),
-		)
 	})
 
 	ginkgo.Context("for BGPAdvertisement", func() {
diff --git a/internal/allocator/allocator_test.go b/internal/allocator/allocator_test.go
index 35e2e0ac..f39951ca 100644
--- a/internal/allocator/allocator_test.go
+++ b/internal/allocator/allocator_test.go
@@ -1229,12 +1229,17 @@ func TestPoolAllocation(t *testing.T) {
 		if err != nil {
 			t.Errorf("%s: AllocateFromPool(%q, \"test\"): %s", test.desc, test.svcKey, err)
 		}
-		validIPs := validIP4s
-		if test.ipFamily == ipfamily.IPv6 {
+
+		var validIPs map[string]bool
+		switch test.ipFamily {
+		case ipfamily.IPv6:
 			validIPs = validIP6s
-		} else if test.ipFamily == ipfamily.DualStack {
+		case ipfamily.DualStack:
 			validIPs = validIPDualStacks
+		default:
+			validIPs = validIP4s
 		}
+
 		for _, ip := range ips {
 			if !validIPs[ip.String()] {
 				t.Errorf("%s: allocated unexpected IP %q", test.desc, ip)
@@ -1605,12 +1610,16 @@ func TestAllocation(t *testing.T) {
 			t.Errorf("%s: Allocate(%q, \"test\"): %s", test.desc, test.svcKey, err)
 		}
 
-		validIPs := validIP4s
-		if test.ipFamily == ipfamily.IPv6 {
+		var validIPs map[string]bool
+		switch test.ipFamily {
+		case ipfamily.IPv6:
 			validIPs = validIP6s
-		} else if test.ipFamily == ipfamily.DualStack {
+		case ipfamily.DualStack:
 			validIPs = validIPDualStacks
+		default:
+			validIPs = validIP4s
 		}
+
 		for _, ip := range ips {
 			if !validIPs[ip.String()] {
 				t.Errorf("%s allocated unexpected IP %q", test.desc, ip)
@@ -2022,11 +2031,14 @@ func TestAutoAssign(t *testing.T) {
 		if err != nil {
 			t.Errorf("#%d Allocate(%q, \"test\"): %s", i+1, test.svcKey, err)
 		}
-		validIPs := validIP4s
-		if test.ipFamily == ipfamily.IPv6 {
+		var validIPs map[string]bool
+		switch test.ipFamily {
+		case ipfamily.IPv6:
 			validIPs = validIP6s
-		} else if test.ipFamily == ipfamily.DualStack {
+		case ipfamily.DualStack:
 			validIPs = validIPDualStacks
+		default:
+			validIPs = validIP4s
 		}
 		for _, ip := range ips {
 			if !validIPs[ip.String()] {
diff --git a/internal/k8s/webhooks/webhookv1beta1/ipaddresspool_webhook_test.go b/internal/k8s/webhooks/webhookv1beta1/ipaddresspool_webhook_test.go
index 7a7ed80f..abc4ad0c 100644
--- a/internal/k8s/webhooks/webhookv1beta1/ipaddresspool_webhook_test.go
+++ b/internal/k8s/webhooks/webhookv1beta1/ipaddresspool_webhook_test.go
@@ -8,7 +8,6 @@ import (
 	"github.com/go-kit/log"
 	"github.com/google/go-cmp/cmp"
 	"go.universe.tf/metallb/api/v1beta1"
-	v1core "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
@@ -30,14 +29,9 @@ func TestValidateIPAddressPool(t *testing.T) {
 			},
 		}, nil
 	}
-	toRestoreNodes := getExistingNodes
-	getExistingNodes = func() (*v1core.NodeList, error) {
-		return &v1core.NodeList{}, nil
-	}
 
 	defer func() {
 		getExistingIPAddressPools = toRestoreIPAddressPools
-		getExistingNodes = toRestoreNodes
 	}()
 
 	tests := []struct {