Repository: metallb/metallb
Pull request #1884 (2023-04-06 02:58:47)

Bump golang.org/x/sys from 0.6.0 to 0.7.0
Bumps [golang.org/x/sys](https://github.com/golang/sys) from 0.6.0 to 0.7.0.
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/golang/sys/commit/64840c112d2335ed9874114aed48f946e778a769"><code>64840c1</code></a> unix: add bindings for setattrlist() on macOS</li>
<li><a href="https://github.com/golang/sys/commit/90abad3705b20ab7ec085959e4fe6f164d873906"><code>90abad3</code></a> unix: add AT_EACCESS on Darwin</li>
<li><a href="https://github.com/golang/sys/commit/94933fc7d00c3c66a066f331c42ad25d21fbb7dd"><code>94933fc</code></a> windows: fix constant values for JobObjectInformationClass</li>
<li><a href="https://github.com/golang/sys/commit/00d8004a14487f8c7b7fdfe44b95e9f6c4590f5f"><code>00d8004</code></a> unix: match ioctl req argument type to libc type</li>
<li><a href="https://github.com/golang/sys/commit/d0781cc69bfe157df30acb4a8774b3a3ad20b2f5"><code>d0781cc</code></a> unix: make solaris syscall tests less flaky</li>
<li><a href="https://github.com/golang/sys/commit/ff18efa0a3fa22d4fede381b822bbcfe53b7ee7c"><code>ff18efa</code></a> unix: change Setrlimit/Prlimit to always call syscall functions</li>
<li><a href="https://github.com/golang/sys/commit/494aa493ccb0c797af52f75ff7839a241bb26cdd"><code>494aa49</code></a> unix: skip ip related tests if EAFNOSUPPORT raised</li>
<li>See full diff in <a href="https://github.com/golang/sys/compare/v0.6.0...v0.7.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/sys&package-manager=go_modules&previous-version=0.6.0&new-version=0.7.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/config/manifests/metallb-frr-prometheus.yaml b/config/manifests/metallb-frr-prometheus.yaml
index 59c2c2a7..652404cf 100644
--- a/config/manifests/metallb-frr-prometheus.yaml
+++ b/config/manifests/metallb-frr-prometheus.yaml
@@ -12,6 +12,7 @@ kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.11.1
+  creationTimestamp: null
   name: addresspools.metallb.io
 spec:
   conversion:
@@ -533,6 +534,7 @@ kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.11.1
+  creationTimestamp: null
   name: bgppeers.metallb.io
 spec:
   conversion:
diff --git a/config/manifests/metallb-frr.yaml b/config/manifests/metallb-frr.yaml
index 18bfbe36..fc1f59c5 100644
--- a/config/manifests/metallb-frr.yaml
+++ b/config/manifests/metallb-frr.yaml
@@ -12,6 +12,7 @@ kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.11.1
+  creationTimestamp: null
   name: addresspools.metallb.io
 spec:
   conversion:
@@ -533,6 +534,7 @@ kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.11.1
+  creationTimestamp: null
   name: bgppeers.metallb.io
 spec:
   conversion:
diff --git a/config/manifests/metallb-native-prometheus.yaml b/config/manifests/metallb-native-prometheus.yaml
index 7b382c57..82199930 100644
--- a/config/manifests/metallb-native-prometheus.yaml
+++ b/config/manifests/metallb-native-prometheus.yaml
@@ -12,6 +12,7 @@ kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.11.1
+  creationTimestamp: null
   name: addresspools.metallb.io
 spec:
   conversion:
@@ -533,6 +534,7 @@ kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.11.1
+  creationTimestamp: null
   name: bgppeers.metallb.io
 spec:
   conversion:
diff --git a/config/manifests/metallb-native.yaml b/config/manifests/metallb-native.yaml
index f34a3c9a..2165f1a4 100644
--- a/config/manifests/metallb-native.yaml
+++ b/config/manifests/metallb-native.yaml
@@ -12,6 +12,7 @@ kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.11.1
+  creationTimestamp: null
   name: addresspools.metallb.io
 spec:
   conversion:
@@ -533,6 +534,7 @@ kind: CustomResourceDefinition
 metadata:
   annotations:
     controller-gen.kubebuilder.io/version: v0.11.1
+  creationTimestamp: null
   name: bgppeers.metallb.io
 spec:
   conversion:
diff --git a/go.mod b/go.mod
index c75fa227..db519eee 100644
--- a/go.mod
+++ b/go.mod
@@ -23,7 +23,7 @@ require (
 	github.com/prometheus/client_model v0.3.0
 	github.com/prometheus/common v0.42.0
 	golang.org/x/sync v0.1.0
-	golang.org/x/sys v0.6.0
+	golang.org/x/sys v0.7.0
 	k8s.io/api v0.26.1
 	k8s.io/apiextensions-apiserver v0.26.1
 	k8s.io/apimachinery v0.26.1
diff --git a/go.sum b/go.sum
index 6f3d2ee4..536f8431 100644
--- a/go.sum
+++ b/go.sum
@@ -755,8 +755,8 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
-golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=
+golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
diff --git a/internal/k8s/controllers/node_controller.go b/internal/k8s/controllers/node_controller.go
index a2ee7eda..cc67477b 100644
--- a/internal/k8s/controllers/node_controller.go
+++ b/internal/k8s/controllers/node_controller.go
@@ -18,6 +18,7 @@ package controllers
 
 import (
 	"context"
+	"reflect"
 
 	"github.com/go-kit/log"
 	"github.com/go-kit/log/level"
@@ -33,11 +34,12 @@ import (
 
 type NodeReconciler struct {
 	client.Client
-	Logger    log.Logger
-	Scheme    *runtime.Scheme
-	NodeName  string
-	Namespace string
-	Handler   func(log.Logger, *corev1.Node) SyncState
+	Logger      log.Logger
+	Scheme      *runtime.Scheme
+	NodeName    string
+	Namespace   string
+	Handler     func(log.Logger, *corev1.Node) SyncState
+	ForceReload func()
 }
 
 func (r *NodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
@@ -57,7 +59,8 @@ func (r *NodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
 		updateErrors.Inc()
 		return ctrl.Result{}, errRetry
 	case SyncStateReprocessAll:
-		level.Error(r.Logger).Log("controller", "NodeReconciler", "error", "unexpected result reprocess all")
+		level.Info(r.Logger).Log("controller", "NodeReconciler", "event", "force service reload")
+		r.ForceReload()
 		return ctrl.Result{}, nil
 	case SyncStateErrorNoRetry:
 		updateErrors.Inc()
@@ -68,15 +71,6 @@ func (r *NodeReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
 
 func (r *NodeReconciler) SetupWithManager(mgr ctrl.Manager) error {
 	p := predicate.Funcs{
-		CreateFunc: func(e event.CreateEvent) bool {
-			return r.filterOtherNodes(e.Object)
-		},
-		DeleteFunc: func(e event.DeleteEvent) bool {
-			return r.filterOtherNodes(e.Object)
-		},
-		GenericFunc: func(e event.GenericEvent) bool {
-			return r.filterOtherNodes(e.Object)
-		},
 		UpdateFunc: func(e event.UpdateEvent) bool {
 			newNodeObj, ok := e.ObjectNew.(*corev1.Node)
 			if !ok {
@@ -89,10 +83,11 @@ func (r *NodeReconciler) SetupWithManager(mgr ctrl.Manager) error {
 				return true
 			}
 			// If there is no changes in node labels, ignore event.
-			if labels.Equals(labels.Set(oldNodeObj.Labels), labels.Set(newNodeObj.Labels)) {
+			if labels.Equals(labels.Set(oldNodeObj.Labels), labels.Set(newNodeObj.Labels)) &&
+				reflect.DeepEqual(oldNodeObj.Status.Conditions, newNodeObj.Status.Conditions) {
 				return false
 			}
-			return r.filterOtherNodes(newNodeObj)
+			return true
 		},
 	}
 	return ctrl.NewControllerManagedBy(mgr).
@@ -100,12 +95,3 @@ func (r *NodeReconciler) SetupWithManager(mgr ctrl.Manager) error {
 		WithEventFilter(p).
 		Complete(r)
 }
-
-func (r *NodeReconciler) filterOtherNodes(obj client.Object) bool {
-	node, ok := obj.(*corev1.Node)
-	if !ok {
-		level.Error(r.Logger).Log("controller", "NodeReconciler", "error", "object is not node", "name", obj.GetName())
-		return false
-	}
-	return node.Name == r.NodeName
-}
diff --git a/internal/k8s/k8s.go b/internal/k8s/k8s.go
index 03d558e2..ba2057b2 100644
--- a/internal/k8s/k8s.go
+++ b/internal/k8s/k8s.go
@@ -200,11 +200,12 @@ func New(cfg *Config) (*Client, error) {
 
 	if cfg.NodeChanged != nil {
 		if err = (&controllers.NodeReconciler{
-			Client:   mgr.GetClient(),
-			Logger:   cfg.Logger,
-			Scheme:   mgr.GetScheme(),
-			Handler:  cfg.NodeHandler,
-			NodeName: cfg.NodeName,
+			Client:      mgr.GetClient(),
+			Logger:      cfg.Logger,
+			Scheme:      mgr.GetScheme(),
+			Handler:     cfg.NodeHandler,
+			NodeName:    cfg.NodeName,
+			ForceReload: reload,
 		}).SetupWithManager(mgr); err != nil {
 			level.Error(c.logger).Log("error", err, "unable to create controller", "node")
 			return nil, errors.Wrap(err, "failed to create node reconciler")
diff --git a/internal/k8s/nodes/nodes.go b/internal/k8s/nodes/nodes.go
new file mode 100644
index 00000000..5d335f47
--- /dev/null
+++ b/internal/k8s/nodes/nodes.go
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier:Apache-2.0
+
+package nodes
+
+import (
+	corev1 "k8s.io/api/core/v1"
+)
+
+// ConditionStatus returns the status of the condition for a given node.
+func ConditionStatus(n *corev1.Node, ct corev1.NodeConditionType) corev1.ConditionStatus {
+	if n == nil {
+		return corev1.ConditionUnknown
+	}
+
+	for _, c := range n.Status.Conditions {
+		if c.Type == ct {
+			return c.Status
+		}
+	}
+
+	return corev1.ConditionUnknown
+}
+
+// IsNetworkUnavailable returns true if the given node NodeNetworkUnavailable condition status is true.
+func IsNetworkUnavailable(n *corev1.Node) bool {
+	return ConditionStatus(n, corev1.NodeNetworkUnavailable) == corev1.ConditionTrue
+}
diff --git a/speaker/bgp_controller.go b/speaker/bgp_controller.go
index 01abe343..b4ffbfef 100644
--- a/speaker/bgp_controller.go
+++ b/speaker/bgp_controller.go
@@ -26,6 +26,7 @@ import (
 	bgpnative "go.universe.tf/metallb/internal/bgp/native"
 	"go.universe.tf/metallb/internal/config"
 	"go.universe.tf/metallb/internal/k8s/epslices"
+	k8snodes "go.universe.tf/metallb/internal/k8s/nodes"
 	"go.universe.tf/metallb/internal/logging"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/labels"
@@ -159,11 +160,16 @@ func hasHealthyEndpoint(eps epslices.EpsOrSlices, filterNode func(*string) bool)
 	return false
 }
 
-func (c *bgpController) ShouldAnnounce(l log.Logger, name string, _ []net.IP, pool *config.Pool, svc *v1.Service, eps epslices.EpsOrSlices) string {
+func (c *bgpController) ShouldAnnounce(l log.Logger, name string, _ []net.IP, pool *config.Pool, svc *v1.Service, eps epslices.EpsOrSlices, nodes map[string]*v1.Node) string {
 	if !poolMatchesNodeBGP(pool, c.myNode) {
 		level.Debug(l).Log("event", "skipping should announce bgp", "service", name, "reason", "pool not matching my node")
 		return "notOwner"
 	}
+
+	if k8snodes.IsNetworkUnavailable(nodes[c.myNode]) {
+		level.Debug(l).Log("event", "skipping should announce bgp", "service", name, "reason", "speaker's node has NodeNetworkUnavailable condition")
+		return "nodeNetworkUnavailable"
+	}
 	// Should we advertise?
 	// Yes, if externalTrafficPolicy is
 	//  Cluster && any healthy endpoint exists
@@ -339,6 +345,9 @@ func (c *bgpController) DeleteBalancer(l log.Logger, name, reason string) error
 }
 
 func (c *bgpController) SetNode(l log.Logger, node *v1.Node) error {
+	if c.myNode != node.Name {
+		return nil
+	}
 	nodeLabels := node.Labels
 	if nodeLabels == nil {
 		nodeLabels = map[string]string{}
diff --git a/speaker/layer2_controller.go b/speaker/layer2_controller.go
index 2501e665..13ade79f 100644
--- a/speaker/layer2_controller.go
+++ b/speaker/layer2_controller.go
@@ -24,6 +24,7 @@ import (
 	"github.com/go-kit/log/level"
 	"go.universe.tf/metallb/internal/config"
 	"go.universe.tf/metallb/internal/k8s/epslices"
+	k8snodes "go.universe.tf/metallb/internal/k8s/nodes"
 	"go.universe.tf/metallb/internal/layer2"
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/util/sets"
@@ -94,7 +95,7 @@ func usableNodes(eps epslices.EpsOrSlices, speakers map[string]bool) []string {
 	return ret
 }
 
-func (c *layer2Controller) ShouldAnnounce(l log.Logger, name string, toAnnounce []net.IP, pool *config.Pool, svc *v1.Service, eps epslices.EpsOrSlices) string {
+func (c *layer2Controller) ShouldAnnounce(l log.Logger, name string, toAnnounce []net.IP, pool *config.Pool, svc *v1.Service, eps epslices.EpsOrSlices, nodes map[string]*v1.Node) string {
 	if !activeEndpointExists(eps) { // no active endpoints, just return
 		level.Debug(l).Log("event", "shouldannounce", "protocol", "l2", "message", "failed no active endpoints", "service", name)
 		return "notOwner"
@@ -106,27 +107,29 @@ func (c *layer2Controller) ShouldAnnounce(l log.Logger, name string, toAnnounce
 	}
 
 	// we select the nodes with at least one matching l2 advertisement
-	forPool := speakersForPool(c.sList.UsableSpeakers(), pool)
-	var nodes []string
+	forPool := speakersForPool(c.sList.UsableSpeakers(), pool, nodes)
+	var availableNodes []string
 	if svc.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeLocal {
-		nodes = usableNodes(eps, forPool)
+		availableNodes = usableNodes(eps, forPool)
 	} else {
-		nodes = nodesWithActiveSpeakers(forPool)
+		availableNodes = nodesWithActiveSpeakers(forPool)
 	}
+	level.Debug(l).Log("event", "shouldannounce", "protocol", "l2", "nodes", availableNodes, "service", name)
+
 	// Using the first IP should work for both single and dual stack.
 	ipString := toAnnounce[0].String()
 	// Sort the slice by the hash of node + load balancer ips. This
 	// produces an ordering of ready nodes that is unique to all the services
 	// with the same ip.
-	sort.Slice(nodes, func(i, j int) bool {
-		hi := sha256.Sum256([]byte(nodes[i] + "#" + ipString))
-		hj := sha256.Sum256([]byte(nodes[j] + "#" + ipString))
+	sort.Slice(availableNodes, func(i, j int) bool {
+		hi := sha256.Sum256([]byte(availableNodes[i] + "#" + ipString))
+		hj := sha256.Sum256([]byte(availableNodes[j] + "#" + ipString))
 
 		return bytes.Compare(hi[:], hj[:]) < 0
 	})
 
 	// Are we first in the list? If so, we win and should announce.
-	if len(nodes) > 0 && nodes[0] == c.myNode {
+	if len(availableNodes) > 0 && availableNodes[0] == c.myNode {
 		return ""
 	}
 
@@ -157,7 +160,10 @@ func (c *layer2Controller) DeleteBalancer(l log.Logger, name, reason string) err
 	return nil
 }
 
-func (c *layer2Controller) SetNode(log.Logger, *v1.Node) error {
+func (c *layer2Controller) SetNode(l log.Logger, n *v1.Node) error {
+	if c.myNode != n.Name {
+		return nil
+	}
 	c.sList.Rejoin()
 	return nil
 }
@@ -216,9 +222,12 @@ func poolMatchesNodeL2(pool *config.Pool, node string) bool {
 	return false
 }
 
-func speakersForPool(speakers map[string]bool, pool *config.Pool) map[string]bool {
+func speakersForPool(speakers map[string]bool, pool *config.Pool, nodes map[string]*v1.Node) map[string]bool {
 	res := map[string]bool{}
 	for s := range speakers {
+		if k8snodes.IsNetworkUnavailable(nodes[s]) {
+			continue
+		}
 		if poolMatchesNodeL2(pool, s) {
 			res[s] = true
 		}
diff --git a/speaker/main.go b/speaker/main.go
index d9b20a43..d27b2952 100644
--- a/speaker/main.go
+++ b/speaker/main.go
@@ -33,6 +33,7 @@ import (
 	"go.universe.tf/metallb/internal/k8s"
 	"go.universe.tf/metallb/internal/k8s/controllers"
 	"go.universe.tf/metallb/internal/k8s/epslices"
+	k8snodes "go.universe.tf/metallb/internal/k8s/nodes"
 	"go.universe.tf/metallb/internal/layer2"
 	"go.universe.tf/metallb/internal/logging"
 	"go.universe.tf/metallb/internal/speakerlist"
@@ -204,6 +205,7 @@ func main() {
 
 type controller struct {
 	myNode  string
+	nodes   map[string]*v1.Node
 	bgpType bgpImplementation
 
 	config *config.Config
@@ -268,6 +270,8 @@ func newController(cfg controllerConfig) (*controller, error) {
 	ret.announced[config.BGP] = map[string]bool{}
 	ret.announced[config.Layer2] = map[string]bool{}
 
+	ret.nodes = make(map[string]*v1.Node)
+
 	return ret, nil
 }
 
@@ -345,7 +349,7 @@ func (c *controller) handleService(l log.Logger,
 		return c.deleteBalancerProtocol(l, protocol, name, "internalError")
 	}
 
-	if deleteReason := handler.ShouldAnnounce(l, name, lbIPs, pool, svc, eps); deleteReason != "" {
+	if deleteReason := handler.ShouldAnnounce(l, name, lbIPs, pool, svc, eps, c.nodes); deleteReason != "" {
 		return c.deleteBalancerProtocol(l, protocol, name, deleteReason)
 	}
 
@@ -411,8 +415,8 @@ func (c *controller) deleteBalancerProtocol(l log.Logger, protocol config.Proto,
 			return controllers.SyncStateSuccess
 		}
 	}
-	delete(c.svcIPs, name)
 	level.Info(l).Log("event", "serviceWithdrawn", "ip", c.svcIPs[name], "reason", reason, "msg", "withdrawing service announcement")
+	delete(c.svcIPs, name)
 
 	return controllers.SyncStateSuccess
 }
@@ -508,19 +512,31 @@ func (c *controller) SetConfig(l log.Logger, cfg *config.Config) controllers.Syn
 }
 
 func (c *controller) SetNode(l log.Logger, node *v1.Node) controllers.SyncState {
+	conditionChanged := isNetworkConditionChanged(node.Name, c.nodes, node)
+	c.nodes[node.Name] = node
+
 	for proto, handler := range c.protocolHandlers {
 		if err := handler.SetNode(l, node); err != nil {
 			level.Error(l).Log("op", "setNode", "error", err, "protocol", proto, "msg", "failed to propagate node info to protocol handler")
 			return controllers.SyncStateError
 		}
 	}
+
+	if conditionChanged {
+		return controllers.SyncStateReprocessAll
+	}
+
 	return controllers.SyncStateSuccess
 }
 
+func isNetworkConditionChanged(nodeName string, oldNodes map[string]*v1.Node, newNode *v1.Node) bool {
+	return k8snodes.IsNetworkUnavailable(oldNodes[nodeName]) != k8snodes.IsNetworkUnavailable(newNode)
+}
+
 // A Protocol can advertise an IP address.
 type Protocol interface {
 	SetConfig(log.Logger, *config.Config) error
-	ShouldAnnounce(log.Logger, string, []net.IP, *config.Pool, *v1.Service, epslices.EpsOrSlices) string
+	ShouldAnnounce(log.Logger, string, []net.IP, *config.Pool, *v1.Service, epslices.EpsOrSlices, map[string]*v1.Node) string
 	SetBalancer(log.Logger, string, []net.IP, *config.Pool, service, *v1.Service) error
 	DeleteBalancer(log.Logger, string, string) error
 	SetNode(log.Logger, *v1.Node) error
diff --git a/tasks.py b/tasks.py
index 6e939ded..0a95b38c 100644
--- a/tasks.py
+++ b/tasks.py
@@ -27,6 +27,8 @@ default_network = "kind"
 extra_network = "network2"
 controller_gen_version = "v0.11.1"
 build_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "build")
+kubectl_path = os.path.join(build_path, "kubectl")
+kubectl_version = "v1.27.0"
 
 def _check_architectures(architectures):
     out = set()
@@ -82,13 +84,6 @@ def run_with_retry(cmd, tries=6, delay=2):
     mdelay *= 2 #exponential backoff
   run(cmd)
 
-def _make_build_dirs():
-    for arch in all_architectures:
-        for binary in all_binaries:
-            dir = os.path.join("build", arch, binary)
-            if not os.path.exists(dir):
-                os.makedirs(dir, mode=0o750)
-
 
 # Returns true if docker is a symbolic link to podman.
 def _is_podman():
@@ -284,6 +279,7 @@ def validate_kind_version():
         raise Exit(message="kind version >= {} required".format(min_version))
 
 def generate_manifest(ctx, crd_options="crd:crdVersions=v1", bgp_type="native", output=None, with_prometheus=False):
+    _fetch_kubectl()
     run("GOPATH={} go install sigs.k8s.io/controller-tools/cmd/controller-gen@{}".format(build_path, controller_gen_version))    
     res = run("{}/bin/controller-gen {} rbac:roleName=manager-role webhook paths=\"./api/...\" output:crd:artifacts:config=config/crd/bases".format(build_path, crd_options))
     if not res.ok:
@@ -293,7 +289,7 @@ def generate_manifest(ctx, crd_options="crd:crdVersions=v1", bgp_type="native",
         layer=bgp_type
         if with_prometheus:
             layer = "prometheus-" + layer
-        res = run("kubectl kustomize config/{} > {}".format(layer, output))
+        res = run("{} kustomize config/{} > {}".format(kubectl_path, layer, output))
         if not res.ok:
             raise Exit(message="Failed to kustomize manifests")
 
@@ -331,6 +327,7 @@ def dev_env(ctx, architecture="amd64", name="kind", protocol=None, frr_volume_di
     The optional node_img parameter will be used to determine the version of the cluster.
     """
 
+    _fetch_kubectl()
     validate_kind_version()
 
     clusters = run("kind get clusters", hide=True).stdout.strip().splitlines()
@@ -373,7 +370,7 @@ def dev_env(ctx, architecture="amd64", name="kind", protocol=None, frr_volume_di
         deployprometheus(ctx)
 
     if helm_install:
-        run("kubectl apply -f config/native/ns.yaml", echo=True)
+        run("{} apply -f config/native/ns.yaml".format(kubectl_path), echo=True)
         prometheus_values=""
         if with_prometheus:
             prometheus_values=("--set prometheus.serviceMonitor.enabled=true "
@@ -386,7 +383,7 @@ def dev_env(ctx, architecture="amd64", name="kind", protocol=None, frr_volume_di
                 "--set controller.logLevel=debug {} --namespace metallb-system".format(architecture, architecture, 
                 "true" if bgp_type == "frr" else "false", prometheus_values), echo=True)
     else:
-        run("kubectl delete po -n metallb-system --all", echo=True)
+        run("{} delete po -n metallb-system --all".format(kubectl_path), echo=True)
 
         with tempfile.TemporaryDirectory() as tmpdir:
             manifest_file = tmpdir + "/metallb.yaml"
@@ -406,7 +403,7 @@ def dev_env(ctx, architecture="amd64", name="kind", protocol=None, frr_volume_di
                 f.write(manifest)
                 f.flush()
 
-            run("kubectl apply -f {}".format(manifest_file), echo=True)
+            run("{} apply -f {}".format(kubectl_path, manifest_file), echo=True)
 
     if protocol == "bgp":
         print("Configuring MetalLB with a BGP test environment")
@@ -422,6 +419,7 @@ def dev_env(ctx, architecture="amd64", name="kind", protocol=None, frr_volume_di
 # Configure MetalLB in the dev-env for layer2 testing.
 # Identify the unused network address range from kind network and used it in configmap.
 def layer2_dev_env():
+    _fetch_kubectl()
     dev_env_dir = os.getcwd() + "/dev-env/layer2"
     with open("%s/config.yaml.tmpl" % dev_env_dir, 'r') as f:
         layer2_config = "# THIS FILE IS AUTOGENERATED\n" + f.read()
@@ -432,12 +430,13 @@ def layer2_dev_env():
     with open("%s/config.yaml" % dev_env_dir, 'w') as f:
         f.write(layer2_config)
     # Apply the MetalLB ConfigMap
-    run("kubectl apply -f %s/config.yaml" % dev_env_dir)
+    run("{} apply -f {}/config.yaml".format(kubectl_path, dev_env_dir))
 
 # Configure MetalLB in the dev-env for BGP testing. Start an frr based BGP
 # router in a container and configure MetalLB to peer with it.
 # See dev-env/bgp/README.md for some more information.
 def bgp_dev_env(ip_family, frr_volume_dir):
+    _fetch_kubectl()
     dev_env_dir = os.getcwd() + "/dev-env/bgp"
     if frr_volume_dir == "":
         frr_volume_dir = dev_env_dir + "/frr-volume"
@@ -447,8 +446,8 @@ def bgp_dev_env(ip_family, frr_volume_dir):
     # We need the IPs for each Node in the cluster to place them in the BGP
     # router configuration file (bgpd.conf). Each Node will peer with this
     # router.
-    node_ips = run("kubectl get nodes -o jsonpath='{.items[*].status.addresses"
-            "[?(@.type==\"InternalIP\")].address}{\"\\n\"}'", echo=True)
+    node_ips = run("{} get nodes -o jsonpath='{{.items[*].status.addresses"
+                   "[?(@.type==\"InternalIP\")].address}}{{\"\\n\"}}'".format(kubectl_path), echo=True)
     node_ips = node_ips.stdout.strip().split()
     if len(node_ips) != 3:
         raise Exit(message='Expected 3 nodes, got %d' % len(node_ips))
@@ -502,7 +501,7 @@ def bgp_dev_env(ip_family, frr_volume_dir):
     with open("%s/config.yaml" % dev_env_dir, 'w') as f:
         f.write(mlb_config)
     # Apply the MetalLB ConfigMap
-    run_with_retry("kubectl apply -f %s/config.yaml" % dev_env_dir)
+    run_with_retry("{} apply -f {}/config.yaml".format(kubectl_path, dev_env_dir))
 
 def get_available_ips(ip_family=None):
     if ip_family is None or (ip_family != 4 and ip_family != 6):
@@ -741,9 +740,12 @@ def helmdocs(ctx, env="container"):
     "local_nics": "a list of bridges related node's interfaces separated by comma, default is kind",
     "external_containers": "a comma separated list of external containers names to use for the test. (valid parameters are: ibgp-single-hop / ibgp-multi-hop / ebgp-single-hop / ebgp-multi-hop)",
     "native_bgp": "tells if the given cluster is deployed using native bgp mode ",
+    "external_frr_image": "overrides the image used for the external frr containers used in tests",
 })
-def e2etest(ctx, name="kind", export=None, kubeconfig=None, system_namespaces="kube-system,metallb-system", service_pod_port=80, skip_docker=False, focus="", skip="", ipv4_service_range=None, ipv6_service_range=None, prometheus_namespace="", node_nics="kind", local_nics="kind", external_containers="", native_bgp=False,):
+def e2etest(ctx, name="kind", export=None, kubeconfig=None, system_namespaces="kube-system,metallb-system", service_pod_port=80, skip_docker=False, focus="", skip="", ipv4_service_range=None, ipv6_service_range=None, prometheus_namespace="", node_nics="kind", local_nics="kind", external_containers="", native_bgp=False,external_frr_image=""):
     """Run E2E tests against development cluster."""
+    _fetch_kubectl()
+
     if skip_docker:
         opt_skip_docker = "--skip-docker"
     else:
@@ -771,7 +773,7 @@ def e2etest(ctx, name="kind", export=None, kubeconfig=None, system_namespaces="k
         
     namespaces = system_namespaces.replace(' ', '').split(',')
     for ns in namespaces:
-        run("kubectl -n {} wait --for=condition=Ready --all pods --timeout 300s".format(ns), hide=True)
+        run("{} -n {} wait --for=condition=Ready --all pods --timeout 300s".format(kubectl_path, ns), hide=True)
 
     if node_nics == "kind":
         nodes = run("kind get nodes --name {name}".format(name=name)).stdout.strip().split("\n")
@@ -800,8 +802,10 @@ def e2etest(ctx, name="kind", export=None, kubeconfig=None, system_namespaces="k
     if external_containers != "":
         external_containers = "--external-containers="+(external_containers)
 
+    if external_frr_image != "":
+        external_frr_image = "--frr-image="+(external_frr_image)
     testrun = run("cd `git rev-parse --show-toplevel`/e2etest &&"
-            "KUBECONFIG={} ginkgo --timeout=3h {} {} -- --provider=local --kubeconfig={} --service-pod-port={} -ipv4-service-range={} -ipv6-service-range={} {} --report-path {} {} -node-nics {} -local-nics {} {}  -bgp-native-mode={}".format(kubeconfig, ginkgo_focus, ginkgo_skip, kubeconfig, service_pod_port, ipv4_service_range, ipv6_service_range, opt_skip_docker, report_path, prometheus_namespace, node_nics, local_nics, external_containers, native_bgp), warn="True")
+            "KUBECONFIG={} ginkgo --timeout=3h {} {} -- --provider=local --kubeconfig={} --service-pod-port={} -ipv4-service-range={} -ipv6-service-range={} {} --report-path {} {} -node-nics {} -local-nics {} {}  -bgp-native-mode={} {}".format(kubeconfig, ginkgo_focus, ginkgo_skip, kubeconfig, service_pod_port, ipv4_service_range, ipv6_service_range, opt_skip_docker, report_path, prometheus_namespace, node_nics, local_nics, external_containers, native_bgp, external_frr_image), warn="True")
 
     if export != None:
         run("kind export logs {}".format(export))
@@ -867,9 +871,28 @@ def checkchanges(ctx, action="check uncommitted files"):
 @task
 def deployprometheus(ctx):
     """Deploys the prometheus operator under the namespace monitoring"""
-    run("kubectl apply --server-side -f dev-env/kube-prometheus/manifests/setup")
-    run("until kubectl get servicemonitors --all-namespaces ; do date; sleep 1; echo ""; done")
-    run("kubectl apply -f dev-env/kube-prometheus/manifests/")
+    _fetch_kubectl()
+    run("{} apply --server-side -f dev-env/kube-prometheus/manifests/setup".format(kubectl_path))
+    run("until {} get servicemonitors --all-namespaces ; do date; sleep 1; echo ""; done".format(kubectl_path))
+    run("{} apply -f dev-env/kube-prometheus/manifests/".format(kubectl_path))
     print("Waiting for prometheus pods to be running")
-    run("kubectl -n monitoring wait --for=condition=Ready --all pods --timeout 300s")
+    run("{} -n monitoring wait --for=condition=Ready --all pods --timeout 300s".format(kubectl_path))
+
+def _fetch_kubectl():
+    if not os.path.exists(build_path):
+        os.makedirs(build_path, mode=0o750)
+    curl_command = "curl -o {} -LO https://dl.k8s.io/release/{}/bin/$(go env GOOS)/$(go env GOARCH)/kubectl".format(kubectl_path, kubectl_version)
+    if not os.path.exists(kubectl_path):
+        run(curl_command)
+        run("chmod +x {}".format(kubectl_path))
+        return
+    version = run("{} version --short".format(kubectl_path), warn=True, hide='both').stdout
+    for line in version.splitlines():
+        if line.startswith("Client Version:"):
+            version = line.split(":")[1].strip()
+            if version == kubectl_version:
+                return
+    run(curl_command)
+    run("chmod +x {}".format(kubectl_path))
+
 


Test patch:
diff --git a/e2etest/bgptests/bgp.go b/e2etest/bgptests/bgp.go
index b0ff3d87..a4ce2b33 100644
--- a/e2etest/bgptests/bgp.go
+++ b/e2etest/bgptests/bgp.go
@@ -147,6 +147,45 @@ var _ = ginkgo.Describe("BGP", func() {
 			}),
 	)
 
+	ginkgo.Describe("Service with ETP=cluster", func() {
+		ginkgo.It("IPV4 - should not be announced from a node with a NetworkUnavailable condition", func() {
+			allNodes, err := cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
+			framework.ExpectNoError(err)
+			nodeToSet := allNodes.Items[0].Name
+
+			_, svc := setupBGPService(f, ipfamily.IPv4, []string{v4PoolAddresses}, FRRContainers, func(svc *corev1.Service) {
+				testservice.TrafficPolicyCluster(svc)
+			})
+			defer testservice.Delete(cs, svc)
+			validateDesiredLB(svc)
+
+			for _, c := range FRRContainers {
+				validateService(svc, allNodes.Items, c)
+			}
+
+			err = k8s.SetNodeCondition(cs, nodeToSet, corev1.NodeNetworkUnavailable, corev1.ConditionTrue)
+			framework.ExpectNoError(err)
+			defer func() {
+				err = k8s.SetNodeCondition(cs, nodeToSet, corev1.NodeNetworkUnavailable, corev1.ConditionFalse)
+				framework.ExpectNoError(err)
+			}()
+
+			ginkgo.By("validating service is not announced from the unavailable node")
+			for _, c := range FRRContainers {
+				Eventually(func() error {
+					return validateServiceNoWait(svc, []corev1.Node{allNodes.Items[0]}, c)
+				}, time.Minute, time.Second).Should(HaveOccurred())
+			}
+
+			ginkgo.By("validating service is announced from the other available nodes")
+			for _, c := range FRRContainers {
+				Eventually(func() error {
+					return validateServiceNoWait(svc, allNodes.Items[1:], c)
+				}, time.Minute, time.Second).ShouldNot(HaveOccurred())
+			}
+		})
+	})
+
 	ginkgo.DescribeTable("A service of protocol load balancer should work with ETP=local", func(pairingIPFamily ipfamily.Family, poolAddresses []string, tweak testservice.Tweak) {
 
 		jig, svc := setupBGPService(f, pairingIPFamily, poolAddresses, FRRContainers, func(svc *corev1.Service) {
diff --git a/e2etest/bgptests/infra_setup.go b/e2etest/bgptests/infra_setup.go
index 3d10b66e..95109a94 100644
--- a/e2etest/bgptests/infra_setup.go
+++ b/e2etest/bgptests/infra_setup.go
@@ -98,8 +98,8 @@ func ExternalContainersSetup(externalContainers string, cs *clientset.Clientset)
 	return res, nil
 }
 
-func HostContainerSetup() ([]*frrcontainer.FRR, error) {
-	config := hostnetContainerConfig()
+func HostContainerSetup(image string) ([]*frrcontainer.FRR, error) {
+	config := hostnetContainerConfig(image)
 	res, err := frrcontainer.Create(config)
 	if err != nil {
 		return nil, err
@@ -121,8 +121,8 @@ func HostContainerSetup() ([]*frrcontainer.FRR, error) {
 	See `e2etest/README.md` for more details.
 */
 
-func KindnetContainersSetup(cs *clientset.Clientset) ([]*frrcontainer.FRR, error) {
-	configs := frrContainersConfigs()
+func KindnetContainersSetup(cs *clientset.Clientset, image string) ([]*frrcontainer.FRR, error) {
+	configs := frrContainersConfigs(image)
 
 	var out string
 	out, err := executor.Host.Exec(executor.ContainerRuntime, "network", "create", defaultNextHopSettings.multiHopNetwork, "--ipv6",
@@ -152,7 +152,7 @@ func KindnetContainersSetup(cs *clientset.Clientset) ([]*frrcontainer.FRR, error
 	* by doing so, the frr container is reacheable only from "inside" the vrf
 */
 
-func VRFContainersSetup(cs *clientset.Clientset) ([]*frrcontainer.FRR, error) {
+func VRFContainersSetup(cs *clientset.Clientset, image string) ([]*frrcontainer.FRR, error) {
 	out, err := executor.Host.Exec(executor.ContainerRuntime, "network", "create", vrfNetwork, "--ipv6",
 		"--driver=bridge", "--subnet=172.31.0.0/16", "--subnet=fc00:f853:ccd:e799::/64")
 	if err != nil && !strings.Contains(out, "already exists") {
@@ -165,7 +165,7 @@ func VRFContainersSetup(cs *clientset.Clientset) ([]*frrcontainer.FRR, error) {
 		return nil, errors.Wrapf(err, "failed to create %s: %s", vrfNextHopSettings.multiHopNetwork, out)
 	}
 
-	config := vrfContainersConfig()
+	config := vrfContainersConfig(image)
 
 	vrfContainers, err := frrcontainer.Create(config)
 	if err != nil {
@@ -336,10 +336,11 @@ func externalContainersConfigs() map[string]frrcontainer.Config {
 	return res
 }
 
-func hostnetContainerConfig() map[string]frrcontainer.Config {
+func hostnetContainerConfig(image string) map[string]frrcontainer.Config {
 	res := make(map[string]frrcontainer.Config)
 	res["ibgp-single-hop"] = frrcontainer.Config{
-		Name: "ibgp-single-hop",
+		Name:  "ibgp-single-hop",
+		Image: image,
 		Neighbor: frrconfig.NeighborConfig{
 			ASN:      metalLBASN,
 			Password: "ibgp-test",
@@ -357,10 +358,11 @@ func hostnetContainerConfig() map[string]frrcontainer.Config {
 	return res
 }
 
-func frrContainersConfigs() map[string]frrcontainer.Config {
+func frrContainersConfigs(image string) map[string]frrcontainer.Config {
 	res := make(map[string]frrcontainer.Config)
 	res["ibgp-single-hop"] = frrcontainer.Config{
-		Name: "ibgp-single-hop",
+		Name:  "ibgp-single-hop",
+		Image: image,
 		Neighbor: frrconfig.NeighborConfig{
 			ASN:      metalLBASN,
 			Password: "ibgp-test",
@@ -376,7 +378,8 @@ func frrContainersConfigs() map[string]frrcontainer.Config {
 		HostIPv6: hostIPv6,
 	}
 	res["ibgp-multi-hop"] = frrcontainer.Config{
-		Name: "ibgp-multi-hop",
+		Name:  "ibgp-multi-hop",
+		Image: image,
 		Neighbor: frrconfig.NeighborConfig{
 			ASN:      metalLBASN,
 			Password: "ibgp-test",
@@ -392,7 +395,8 @@ func frrContainersConfigs() map[string]frrcontainer.Config {
 		HostIPv6: hostIPv6,
 	}
 	res["ebgp-multi-hop"] = frrcontainer.Config{
-		Name: "ebgp-multi-hop",
+		Name:  "ebgp-multi-hop",
+		Image: image,
 		Neighbor: frrconfig.NeighborConfig{
 			ASN:      metalLBASN,
 			Password: "ebgp-test",
@@ -409,6 +413,7 @@ func frrContainersConfigs() map[string]frrcontainer.Config {
 	}
 	res["ebgp-single-hop"] = frrcontainer.Config{
 		Name:    "ebgp-single-hop",
+		Image:   image,
 		Network: kindNetwork,
 		Neighbor: frrconfig.NeighborConfig{
 			ASN:      metalLBASN,
@@ -422,10 +427,11 @@ func frrContainersConfigs() map[string]frrcontainer.Config {
 	return res
 }
 
-func vrfContainersConfig() map[string]frrcontainer.Config {
+func vrfContainersConfig(image string) map[string]frrcontainer.Config {
 	res := make(map[string]frrcontainer.Config)
 	res["ebgp-vrf-single-hop"] = frrcontainer.Config{
 		Name:    "ebgp-vrf-single-hop",
+		Image:   image,
 		Network: vrfNetwork,
 		Neighbor: frrconfig.NeighborConfig{
 			ASN:      metalLBASNVRF,
@@ -441,6 +447,7 @@ func vrfContainersConfig() map[string]frrcontainer.Config {
 	}
 	res["ibgp-vrf-single-hop"] = frrcontainer.Config{
 		Name:    "ibgp-vrf-single-hop",
+		Image:   image,
 		Network: vrfNetwork,
 		Neighbor: frrconfig.NeighborConfig{
 			ASN:      metalLBASNVRF,
@@ -455,7 +462,8 @@ func vrfContainersConfig() map[string]frrcontainer.Config {
 		},
 	}
 	res["ibgp-vrf-multi-hop"] = frrcontainer.Config{
-		Name: "ibgp-vrf-multi-hop",
+		Name:  "ibgp-vrf-multi-hop",
+		Image: image,
 		Neighbor: frrconfig.NeighborConfig{
 			ASN:      metalLBASNVRF,
 			Password: "ibgp-test",
@@ -470,7 +478,8 @@ func vrfContainersConfig() map[string]frrcontainer.Config {
 		Network: vrfNextHopSettings.multiHopNetwork,
 	}
 	res["ebgp-vrf-multi-hop"] = frrcontainer.Config{
-		Name: "ebgp-vrf-multi-hop",
+		Name:  "ebgp-vrf-multi-hop",
+		Image: image,
 		Neighbor: frrconfig.NeighborConfig{
 			ASN:      metalLBASNVRF,
 			Password: "ebgp-test",
diff --git a/e2etest/e2etest_suite_test.go b/e2etest/e2etest_suite_test.go
index d801a9ec..034021a4 100644
--- a/e2etest/e2etest_suite_test.go
+++ b/e2etest/e2etest_suite_test.go
@@ -55,6 +55,7 @@ var (
 	externalContainers  string
 	runOnHost           bool
 	bgpNativeMode       bool
+	frrImage            string
 )
 
 // handleFlags sets up all flags and parses the command line.
@@ -82,6 +83,7 @@ func handleFlags() {
 	flag.StringVar(&prometheusNamespace, "prometheus-namespace", "monitoring", "the namespace prometheus is running in (if running)")
 	flag.StringVar(&externalContainers, "external-containers", "", "a comma separated list of external containers names to use for the test. (valid parameters are: ibgp-single-hop / ibgp-multi-hop / ebgp-single-hop / ebgp-multi-hop)")
 	flag.BoolVar(&bgpNativeMode, "bgp-native-mode", false, "says if we are testing against a deployment using bgp native mode")
+	flag.StringVar(&frrImage, "frr-image", "quay.io/frrouting/frr:8.4.2", "the image to use for the external frr containers")
 
 	flag.Parse()
 
@@ -131,13 +133,13 @@ var _ = ginkgo.BeforeSuite(func() {
 		bgptests.FRRContainers, err = bgptests.ExternalContainersSetup(externalContainers, cs)
 		framework.ExpectNoError(err)
 	case runOnHost:
-		bgptests.FRRContainers, err = bgptests.HostContainerSetup()
+		bgptests.FRRContainers, err = bgptests.HostContainerSetup(frrImage)
 		framework.ExpectNoError(err)
 	default:
-		bgptests.FRRContainers, err = bgptests.KindnetContainersSetup(cs)
+		bgptests.FRRContainers, err = bgptests.KindnetContainersSetup(cs, frrImage)
 		framework.ExpectNoError(err)
 		if !bgpNativeMode {
-			vrfFRRContainers, err := bgptests.VRFContainersSetup(cs)
+			vrfFRRContainers, err := bgptests.VRFContainersSetup(cs, frrImage)
 			framework.ExpectNoError(err)
 			bgptests.FRRContainers = append(bgptests.FRRContainers, vrfFRRContainers...)
 		}
diff --git a/e2etest/l2tests/assignment.go b/e2etest/l2tests/assignment.go
index 00348d4b..1000232d 100644
--- a/e2etest/l2tests/assignment.go
+++ b/e2etest/l2tests/assignment.go
@@ -30,8 +30,7 @@ var (
 		"first-ns": "true",
 	}
 	secondNsLabels = map[string]string{
-		"second-ns":                    "true",
-		admissionapi.EnforceLevelLabel: string(admissionapi.LevelPrivileged),
+		"second-ns": "true",
 	}
 )
 
@@ -66,7 +65,11 @@ var _ = ginkgo.Describe("IP Assignment", func() {
 		framework.ExpectNoError(err)
 
 		ginkgo.By("Creating a second namespace")
-		err = k8s.CreateNamespace(cs, secondNamespace, secondNsLabels)
+
+		err = k8s.CreateNamespace(cs, secondNamespace, secondNsLabels, func(ns *v1.Namespace) {
+			// we also need to set the pod security policy for the namespace
+			ns.Labels[admissionapi.EnforceLevelLabel] = string(admissionapi.LevelPrivileged)
+		})
 		framework.ExpectNoError(err)
 
 	})
diff --git a/e2etest/l2tests/interface_selector.go b/e2etest/l2tests/interface_selector.go
index e0177b0d..8672db24 100644
--- a/e2etest/l2tests/interface_selector.go
+++ b/e2etest/l2tests/interface_selector.go
@@ -293,21 +293,11 @@ var _ = ginkgo.Describe("L2-interface selector", func() {
 				framework.ExpectNoError(err)
 
 				gomega.Eventually(func() string {
-					err := mac.RequestAddressResolutionFromIface(ingressIP, LocalNics[0], executor.Host)
+					node, err := nodeForService(svc, allNodes.Items)
 					if err != nil {
-						return err.Error()
+						return ""
 					}
-					err = service.ValidateL2(svc)
-					if err != nil {
-						return err.Error()
-					}
-
-					advNode, err := advertisingNodeFromMAC(allNodes.Items, ingressIP, executor.Host)
-					if err != nil {
-						return err.Error()
-					}
-
-					return advNode.Name
+					return node
 				}, 1*time.Minute, 1*time.Second).Should(gomega.Equal(node.Name))
 			}
 		})
diff --git a/e2etest/l2tests/l2.go b/e2etest/l2tests/l2.go
index 9503ff8c..523c347b 100644
--- a/e2etest/l2tests/l2.go
+++ b/e2etest/l2tests/l2.go
@@ -232,6 +232,58 @@ var _ = ginkgo.Describe("L2", func() {
 			err = wget.Do(address, executor.Host)
 			framework.ExpectNoError(err)
 		})
+
+		ginkgo.It("should not be announced from a node with a NetworkUnavailable condition", func() {
+			svc, _ := service.CreateWithBackend(cs, f.Namespace.Name, "external-local-lb", service.TrafficPolicyCluster)
+			defer func() {
+				err := cs.CoreV1().Services(svc.Namespace).Delete(context.TODO(), svc.Name, metav1.DeleteOptions{})
+				framework.ExpectNoError(err)
+			}()
+
+			allNodes, err := cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
+			framework.ExpectNoError(err)
+
+			ginkgo.By("getting the advertising node")
+			var nodeToSet string
+
+			gomega.Eventually(func() error {
+				node, err := nodeForService(svc, allNodes.Items)
+				if err != nil {
+					return err
+				}
+				nodeToSet = node
+				return nil
+			}, 3*time.Minute, time.Second).ShouldNot(gomega.HaveOccurred())
+
+			err = k8s.SetNodeCondition(cs, nodeToSet, corev1.NodeNetworkUnavailable, corev1.ConditionTrue)
+			framework.ExpectNoError(err)
+			defer func() {
+				err = k8s.SetNodeCondition(cs, nodeToSet, corev1.NodeNetworkUnavailable, corev1.ConditionFalse)
+				framework.ExpectNoError(err)
+			}()
+
+			ginkgo.By("validating the service is announced from a different node")
+			gomega.Eventually(func() string {
+				node, err := nodeForService(svc, allNodes.Items)
+				if err != nil {
+					return err.Error()
+				}
+				return node
+			}, time.Minute, time.Second).ShouldNot(gomega.Equal(nodeToSet))
+
+			ginkgo.By("setting the NetworkUnavailable condition back to false")
+			err = k8s.SetNodeCondition(cs, nodeToSet, corev1.NodeNetworkUnavailable, corev1.ConditionFalse)
+			framework.ExpectNoError(err)
+
+			ginkgo.By("validating the service is announced back again from the previous node")
+			gomega.Eventually(func() string {
+				node, err := nodeForService(svc, allNodes.Items)
+				if err != nil {
+					return err.Error()
+				}
+				return node
+			}, time.Minute, time.Second).Should(gomega.Equal(nodeToSet))
+		})
 	})
 
 	ginkgo.Context("validate different AddressPools for type=Loadbalancer", func() {
diff --git a/e2etest/l2tests/node_selector.go b/e2etest/l2tests/node_selector.go
index e6fbc148..9867e49e 100644
--- a/e2etest/l2tests/node_selector.go
+++ b/e2etest/l2tests/node_selector.go
@@ -5,8 +5,6 @@ package l2tests
 import (
 	"context"
 	"fmt"
-	"net"
-	"strconv"
 	"time"
 
 	"github.com/onsi/ginkgo/v2"
@@ -16,7 +14,6 @@ import (
 	"go.universe.tf/metallb/e2etest/pkg/k8s"
 	"go.universe.tf/metallb/e2etest/pkg/mac"
 	"go.universe.tf/metallb/e2etest/pkg/service"
-	"go.universe.tf/metallb/e2etest/pkg/wget"
 
 	internalconfig "go.universe.tf/metallb/internal/config"
 	corev1 "k8s.io/api/core/v1"
@@ -87,9 +84,6 @@ var _ = ginkgo.Describe("L2", func() {
 				framework.ExpectNoError(err)
 			}()
 
-			ingressIP := e2eservice.GetIngressPoint(
-				&svc.Status.LoadBalancer.Ingress[0])
-
 			allNodes, err := cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
 			framework.ExpectNoError(err)
 			for _, node := range allNodes.Items {
@@ -110,22 +104,12 @@ var _ = ginkgo.Describe("L2", func() {
 				err := ConfigUpdater.Update(resources)
 				framework.ExpectNoError(err)
 
-				port := strconv.Itoa(int(svc.Spec.Ports[0].Port))
-				hostport := net.JoinHostPort(ingressIP, port)
-				address := fmt.Sprintf("http://%s/", hostport)
-
 				gomega.Eventually(func() string {
-					err := mac.RequestAddressResolution(ingressIP, executor.Host)
-					if err != nil {
-						return err.Error()
-					}
-					err = wget.Do(address, executor.Host)
-					framework.ExpectNoError(err)
-					advNode, err := advertisingNodeFromMAC(allNodes.Items, ingressIP, executor.Host)
+					node, err := nodeForService(svc, allNodes.Items)
 					if err != nil {
-						return err.Error()
+						return ""
 					}
-					return advNode.Name
+					return node
 				}, 30*time.Second, 1*time.Second).Should(gomega.Equal(node.Name))
 			}
 		})
@@ -160,9 +144,6 @@ var _ = ginkgo.Describe("L2", func() {
 				})
 			framework.ExpectNoError(err)
 
-			ingressIP := e2eservice.GetIngressPoint(
-				&svc.Status.LoadBalancer.Ingress[0])
-
 			l2Advertisements := []metallbv1beta1.L2Advertisement{
 				{
 					ObjectMeta: metav1.ObjectMeta{
@@ -187,23 +168,13 @@ var _ = ginkgo.Describe("L2", func() {
 			framework.ExpectNoError(err)
 
 			ginkgo.By("checking connectivity to its external VIP")
-			port := strconv.Itoa(int(svc.Spec.Ports[0].Port))
-			hostport := net.JoinHostPort(ingressIP, port)
-			address := fmt.Sprintf("http://%s/", hostport)
 
 			gomega.Eventually(func() string {
-				err := mac.RequestAddressResolution(ingressIP, executor.Host)
+				node, err := nodeForService(svc, allNodes.Items)
 				if err != nil {
 					return err.Error()
 				}
-				err = wget.Do(address, executor.Host)
-				framework.ExpectNoError(err)
-
-				advNode, err := advertisingNodeFromMAC(allNodes.Items, ingressIP, executor.Host)
-				if err != nil {
-					return err.Error()
-				}
-				return advNode.Name
+				return node
 			}, 2*time.Minute, time.Second).Should(gomega.Equal(allNodes.Items[0].Name))
 		})
 
@@ -240,10 +211,6 @@ var _ = ginkgo.Describe("L2", func() {
 			err := ConfigUpdater.Update(resources)
 			framework.ExpectNoError(err)
 
-			port := strconv.Itoa(int(svc.Spec.Ports[0].Port))
-			hostport := net.JoinHostPort(ingressIP, port)
-			address := fmt.Sprintf("http://%s/", hostport)
-
 			allNodes, err := cs.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{})
 			framework.ExpectNoError(err)
 
@@ -259,17 +226,11 @@ var _ = ginkgo.Describe("L2", func() {
 			ginkgo.By(fmt.Sprintf("Validating service IP advertised by %s", nodeToLabel.Name))
 
 			gomega.Eventually(func() string {
-				err := mac.RequestAddressResolution(ingressIP, executor.Host)
-				if err != nil {
-					return err.Error()
-				}
-				err = wget.Do(address, executor.Host)
-				framework.ExpectNoError(err)
-				advNode, err := advertisingNodeFromMAC(allNodes.Items, ingressIP, executor.Host)
+				node, err := nodeForService(svc, allNodes.Items)
 				if err != nil {
 					return err.Error()
 				}
-				return advNode.Name
+				return node
 			}, 2*time.Minute, time.Second).Should(gomega.Equal(nodeToLabel.Name))
 		})
 	})
diff --git a/e2etest/l2tests/node_service.go b/e2etest/l2tests/node_service.go
new file mode 100644
index 00000000..7a58405a
--- /dev/null
+++ b/e2etest/l2tests/node_service.go
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier:Apache-2.0
+
+package l2tests
+
+import (
+	"fmt"
+	"net"
+	"strconv"
+
+	"go.universe.tf/metallb/e2etest/pkg/executor"
+	"go.universe.tf/metallb/e2etest/pkg/mac"
+	"go.universe.tf/metallb/e2etest/pkg/wget"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/kubernetes/test/e2e/framework"
+	e2eservice "k8s.io/kubernetes/test/e2e/framework/service"
+)
+
+func nodeForService(svc *corev1.Service, nodes []corev1.Node) (string, error) {
+	ingressIP := e2eservice.GetIngressPoint(&svc.Status.LoadBalancer.Ingress[0])
+
+	port := strconv.Itoa(int(svc.Spec.Ports[0].Port))
+	hostport := net.JoinHostPort(ingressIP, port)
+	address := fmt.Sprintf("http://%s/", hostport)
+	err := mac.RequestAddressResolution(ingressIP, executor.Host)
+	if err != nil {
+		return "", err
+	}
+	err = wget.Do(address, executor.Host)
+	framework.ExpectNoError(err)
+	advNode, err := advertisingNodeFromMAC(nodes, ingressIP, executor.Host)
+	if err != nil {
+		return "", err
+	}
+
+	return advNode.Name, nil
+}
diff --git a/e2etest/pkg/frr/container/container.go b/e2etest/pkg/frr/container/container.go
index 2c597dd4..39dbd279 100644
--- a/e2etest/pkg/frr/container/container.go
+++ b/e2etest/pkg/frr/container/container.go
@@ -25,8 +25,6 @@ import (
 const (
 	// BGP configuration directory.
 	frrConfigDir = "config/frr"
-	// FRR routing image.
-	frrImage = "quay.io/frrouting/frr:8.4.2"
 	// Host network name.
 	hostNetwork = "host"
 	// FRR container mount destination path.
@@ -47,6 +45,7 @@ type FRR struct {
 
 type Config struct {
 	Name     string
+	Image    string
 	Neighbor frrconfig.NeighborConfig
 	Router   frrconfig.RouterConfig
 	HostIPv4 string
@@ -170,7 +169,7 @@ func start(cfg Config) (*FRR, error) {
 	}
 
 	volume := fmt.Sprintf("%s:%s", testDirName, frrMountPath)
-	args := []string{"run", "-d", "--privileged", "--network", cfg.Network, "--rm", "--ulimit", "core=-1", "--name", cfg.Name, "--volume", volume, frrImage}
+	args := []string{"run", "-d", "--privileged", "--network", cfg.Network, "--rm", "--ulimit", "core=-1", "--name", cfg.Name, "--volume", volume, cfg.Image}
 	out, err := exec.Command(executor.ContainerRuntime, args...).CombinedOutput()
 	if err != nil {
 		return nil, errors.Wrapf(err, "failed to start %s container. %s", cfg.Name, out)
diff --git a/e2etest/pkg/k8s/namespace.go b/e2etest/pkg/k8s/namespace.go
index 0e064df9..f4b75577 100644
--- a/e2etest/pkg/k8s/namespace.go
+++ b/e2etest/pkg/k8s/namespace.go
@@ -13,13 +13,21 @@ import (
 	clientset "k8s.io/client-go/kubernetes"
 )
 
-func CreateNamespace(cs clientset.Interface, name string, labels map[string]string) error {
-	_, err := cs.CoreV1().Namespaces().Create(context.Background(), &corev1.Namespace{
+func CreateNamespace(cs clientset.Interface, name string, labels map[string]string, options ...func(*corev1.Namespace)) error {
+	ns := &corev1.Namespace{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:   name,
-			Labels: labels,
+			Labels: make(map[string]string),
 		},
-	}, metav1.CreateOptions{})
+	}
+	for k, v := range labels {
+		ns.Labels[k] = v
+	}
+	for _, option := range options {
+		option(ns)
+	}
+
+	_, err := cs.CoreV1().Namespaces().Create(context.Background(), ns, metav1.CreateOptions{})
 	return err
 }
 
diff --git a/e2etest/pkg/k8s/nodes.go b/e2etest/pkg/k8s/nodes.go
index 94139233..671c6ece 100644
--- a/e2etest/pkg/k8s/nodes.go
+++ b/e2etest/pkg/k8s/nodes.go
@@ -4,11 +4,17 @@ package k8s
 
 import (
 	"context"
+	"encoding/json"
+	"fmt"
 	"net"
+	"time"
 
+	"github.com/onsi/ginkgo/v2"
+	"github.com/onsi/gomega"
 	"go.universe.tf/metallb/e2etest/pkg/executor"
 	"go.universe.tf/metallb/e2etest/pkg/netdev"
 	"go.universe.tf/metallb/internal/ipfamily"
+	"go.universe.tf/metallb/internal/k8s/nodes"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	clientset "k8s.io/client-go/kubernetes"
@@ -91,3 +97,44 @@ func RemoveLabelFromNode(nodeName, key string, cs clientset.Interface) {
 	_, err = cs.CoreV1().Nodes().Update(context.Background(), nodeObject, metav1.UpdateOptions{})
 	framework.ExpectNoError(err)
 }
+
+// SetNodeCondition sets the node's condition to the desired status and validates that the change is applied.
+func SetNodeCondition(cs clientset.Interface, nodeName string, conditionType v1.NodeConditionType, status v1.ConditionStatus) error {
+	ginkgo.By(fmt.Sprintf("setting the %s condition to %s on node %s", conditionType, status, nodeName))
+
+	condition := v1.NodeCondition{
+		Type:               conditionType,
+		Status:             status,
+		Reason:             "Testing",
+		Message:            fmt.Sprintf("This condition is %s for testing", status),
+		LastTransitionTime: metav1.Now(),
+		LastHeartbeatTime:  metav1.Now(),
+	}
+
+	raw, err := json.Marshal(&[]v1.NodeCondition{condition})
+	if err != nil {
+		return fmt.Errorf("failed to set condition %s on node %s: %s", conditionType, nodeName, err)
+	}
+
+	gomega.Eventually(func() error {
+		patch := []byte(fmt.Sprintf(`{"status":{"conditions":%s}}`, raw))
+		_, err = cs.CoreV1().Nodes().PatchStatus(context.Background(), nodeName, patch)
+		if err != nil {
+			return fmt.Errorf("failed to set condition %s on node %s: %s", conditionType, nodeName, err)
+		}
+
+		n, err := cs.CoreV1().Nodes().Get(context.Background(), nodeName, metav1.GetOptions{})
+		if err != nil {
+			return fmt.Errorf("failed to get node %s: %s", nodeName, err)
+		}
+
+		gotStatus := nodes.ConditionStatus(n, conditionType)
+		if status != gotStatus {
+			return fmt.Errorf("failed: got unexpected %s status on node %s", conditionType, nodeName)
+		}
+
+		return nil
+	}, time.Minute, 3*time.Second).ShouldNot(gomega.HaveOccurred())
+
+	return nil
+}
diff --git a/internal/k8s/controllers/node_controller_test.go b/internal/k8s/controllers/node_controller_test.go
index a7a12531..1c6bf03a 100644
--- a/internal/k8s/controllers/node_controller_test.go
+++ b/internal/k8s/controllers/node_controller_test.go
@@ -48,34 +48,39 @@ func TestNodeController(t *testing.T) {
 		},
 	}
 	tests := []struct {
-		desc                 string
-		handlerRes           SyncState
-		expectReconcileFails bool
-		initObjects          []client.Object
+		desc                    string
+		handlerRes              SyncState
+		expectReconcileFails    bool
+		initObjects             []client.Object
+		expectForceReloadCalled bool
 	}{
 		{
-			desc:                 "handler returns SyncStateSuccess",
-			handlerRes:           SyncStateSuccess,
-			initObjects:          []client.Object{testNode},
-			expectReconcileFails: false,
+			desc:                    "handler returns SyncStateSuccess",
+			handlerRes:              SyncStateSuccess,
+			initObjects:             []client.Object{testNode},
+			expectReconcileFails:    false,
+			expectForceReloadCalled: false,
 		},
 		{
-			desc:                 "handler returns SyncStateError",
-			handlerRes:           SyncStateError,
-			initObjects:          []client.Object{testNode},
-			expectReconcileFails: true,
+			desc:                    "handler returns SyncStateError",
+			handlerRes:              SyncStateError,
+			initObjects:             []client.Object{testNode},
+			expectReconcileFails:    true,
+			expectForceReloadCalled: false,
 		},
 		{
-			desc:                 "handler returns SyncStateErrorNoRetry",
-			handlerRes:           SyncStateErrorNoRetry,
-			initObjects:          []client.Object{testNode},
-			expectReconcileFails: false,
+			desc:                    "handler returns SyncStateErrorNoRetry",
+			handlerRes:              SyncStateErrorNoRetry,
+			initObjects:             []client.Object{testNode},
+			expectReconcileFails:    false,
+			expectForceReloadCalled: false,
 		},
 		{
-			desc:                 "handler returns SyncStateReprocessAll",
-			handlerRes:           SyncStateReprocessAll,
-			initObjects:          []client.Object{testNode},
-			expectReconcileFails: false,
+			desc:                    "handler returns SyncStateReprocessAll",
+			handlerRes:              SyncStateReprocessAll,
+			initObjects:             []client.Object{testNode},
+			expectReconcileFails:    false,
+			expectForceReloadCalled: true,
 		},
 	}
 	for _, test := range tests {
@@ -92,13 +97,17 @@ func TestNodeController(t *testing.T) {
 			return test.handlerRes
 		}
 
+		calledForceReload := false
+		mockForceReload := func() { calledForceReload = true }
+
 		r := &NodeReconciler{
-			Client:    fakeClient,
-			Logger:    log.NewNopLogger(),
-			Scheme:    scheme,
-			NodeName:  testNodeName,
-			Namespace: testNamespace,
-			Handler:   mockHandler,
+			Client:      fakeClient,
+			Logger:      log.NewNopLogger(),
+			Scheme:      scheme,
+			NodeName:    testNodeName,
+			Namespace:   testNamespace,
+			Handler:     mockHandler,
+			ForceReload: mockForceReload,
 		}
 		req := reconcile.Request{
 			NamespacedName: types.NamespacedName{
@@ -114,6 +123,10 @@ func TestNodeController(t *testing.T) {
 			t.Errorf("test %s failed: fail reconcile expected: %v, got: %v. err: %v",
 				test.desc, test.expectReconcileFails, failedReconcile, err)
 		}
+
+		if test.expectForceReloadCalled != calledForceReload {
+			t.Errorf("test %s failed: call force reload expected: %v, got: %v", test.desc, test.expectForceReloadCalled, calledForceReload)
+		}
 	}
 }
 
diff --git a/speaker/bgp_controller_test.go b/speaker/bgp_controller_test.go
index 9fc7869b..a1a10833 100644
--- a/speaker/bgp_controller_test.go
+++ b/speaker/bgp_controller_test.go
@@ -1934,6 +1934,7 @@ func TestNodeSelectors(t *testing.T) {
 			desc: "Add node label that matches",
 			node: &v1.Node{
 				ObjectMeta: metav1.ObjectMeta{
+					Name: "pandora",
 					Labels: map[string]string{
 						"foo": "bar",
 					},
@@ -1949,6 +1950,7 @@ func TestNodeSelectors(t *testing.T) {
 			desc: "Change node label so it no longer matches",
 			node: &v1.Node{
 				ObjectMeta: metav1.ObjectMeta{
+					Name: "pandora",
 					Labels: map[string]string{
 						"foo": "baz",
 					},
@@ -1986,6 +1988,7 @@ func TestNodeSelectors(t *testing.T) {
 			desc: "Change node label back, still matches",
 			node: &v1.Node{
 				ObjectMeta: metav1.ObjectMeta{
+					Name: "pandora",
 					Labels: map[string]string{
 						"foo": "bar",
 					},
@@ -2025,6 +2028,7 @@ func TestNodeSelectors(t *testing.T) {
 			desc: "Change node labels to match the other selector",
 			node: &v1.Node{
 				ObjectMeta: metav1.ObjectMeta{
+					Name: "pandora",
 					Labels: map[string]string{
 						"host": "frontend",
 					},
diff --git a/speaker/layer2_controller_test.go b/speaker/layer2_controller_test.go
index 99243019..4c8815de 100644
--- a/speaker/layer2_controller_test.go
+++ b/speaker/layer2_controller_test.go
@@ -1161,8 +1161,8 @@ func TestShouldAnnounce(t *testing.T) {
 		for _, svc := range test.svcs {
 			lbIP := net.ParseIP(svc.Status.LoadBalancer.Ingress[0].IP)
 			lbIPStr := lbIP.String()
-			response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "balancer", []net.IP{lbIP}, test.config.Pools.ByName["default"], svc, test.eps[lbIPStr])
-			response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "balancer", []net.IP{lbIP}, test.config.Pools.ByName["default"], svc, test.eps[lbIPStr])
+			response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "balancer", []net.IP{lbIP}, test.config.Pools.ByName["default"], svc, test.eps[lbIPStr], nil)
+			response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "balancer", []net.IP{lbIP}, test.config.Pools.ByName["default"], svc, test.eps[lbIPStr], nil)
 			if response1 != test.c1ExpectedResult[lbIPStr] {
 				t.Errorf("%q: shouldAnnounce for controller 1 for service %s returned incorrect result, expected '%s', but received '%s'", test.desc, lbIPStr, test.c1ExpectedResult[lbIPStr], response1)
 			}
@@ -2096,8 +2096,8 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 		for _, svc := range test.svcs {
 			lbIP := net.ParseIP(svc.Status.LoadBalancer.Ingress[0].IP)
 			lbIPStr := lbIP.String()
-			response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, test.config.Pools.ByName["default"], svc, test.eps[lbIPStr])
-			response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, test.config.Pools.ByName["default"], svc, test.eps[lbIPStr])
+			response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, test.config.Pools.ByName["default"], svc, test.eps[lbIPStr], nil)
+			response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, test.config.Pools.ByName["default"], svc, test.eps[lbIPStr], nil)
 			if response1 != test.c1ExpectedResult[lbIPStr] {
 				t.Errorf("%q: shouldAnnounce for controller 1 for service %s returned incorrect result, expected '%s', but received '%s'", test.desc, lbIPStr, test.c1ExpectedResult[lbIPStr], response1)
 			}
@@ -2333,8 +2333,8 @@ func TestShouldAnnounceNodeSelector(t *testing.T) {
 
 		lbIP := net.ParseIP(svc.Status.LoadBalancer.Ingress[0].IP)
 		lbIPStr := lbIP.String()
-		response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], &svc, test.eps[lbIPStr])
-		response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], &svc, test.eps[lbIPStr])
+		response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], &svc, test.eps[lbIPStr], nil)
+		response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], &svc, test.eps[lbIPStr], nil)
 		if response1 != test.c1ExpectedResult[lbIPStr] {
 			t.Errorf("%q: shouldAnnounce for controller 1 for service %s returned incorrect result, expected '%s', but received '%s'", test.desc, lbIPStr, test.c1ExpectedResult[lbIPStr], response1)
 		}
@@ -2467,11 +2467,11 @@ func TestClusterPolicy(t *testing.T) {
 		}
 
 		lbIP := net.ParseIP(ip)
-		response1svc1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], svc1, eps1)
-		response2svc1 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], svc1, eps1)
+		response1svc1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], svc1, eps1, nil)
+		response2svc1 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], svc1, eps1, nil)
 
-		response1svc2 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], svc2, eps2)
-		response2svc2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], svc2, eps2)
+		response1svc2 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], svc2, eps2, nil)
+		response2svc2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], svc2, eps2, nil)
 
 		// We check that only one speaker announces the service, so their response must be different
 		if response1svc1 == response2svc1 {
diff --git a/speaker/speaker_test.go b/speaker/speaker_test.go
index e81efcad..c7d24a36 100644
--- a/speaker/speaker_test.go
+++ b/speaker/speaker_test.go
@@ -172,7 +172,7 @@ func (m *MockProtocol) SetConfig(l log.Logger, c *config.Config) error {
 	return nil
 }
 
-func (m *MockProtocol) ShouldAnnounce(_ log.Logger, _ string, _ []net.IP, _ *config.Pool, _ *v1.Service, _ epslices.EpsOrSlices) string {
+func (m *MockProtocol) ShouldAnnounce(_ log.Logger, _ string, _ []net.IP, _ *config.Pool, _ *v1.Service, _ epslices.EpsOrSlices, _ map[string]*v1.Node) string {
 	if m.shouldAnnounce {
 		return ""
 	}