Repository: metallb/metallb
Pull request #2280 (2024-02-12 15:33:20)

Provide a ignore-exclude-lb flag
MetalLB now honors the node.kubernetes.io/exclude-from-external-load-balancers
but this might harm some deployments where there is only one node and
kubeadm still labels the control plane with such label.

Here we provide a knob to disable the behaviour and make metallb work in
such scenarios.


fedepaol: Fixes https://github.com/metallb/metallb/issues/2274

Patch:
diff --git a/charts/metallb/README.md b/charts/metallb/README.md
index 483eb921..929f7a93 100644
--- a/charts/metallb/README.md
+++ b/charts/metallb/README.md
@@ -124,6 +124,7 @@ Kubernetes: `>= 1.19.0-0`
 | speaker.frr.metricsPort | int | `7473` |  |
 | speaker.frr.resources | object | `{}` |  |
 | speaker.frrMetrics.resources | object | `{}` |  |
+| speaker.ignoreExcludeLB | bool | `false` |  |
 | speaker.image.pullPolicy | string | `nil` |  |
 | speaker.image.repository | string | `"quay.io/metallb/speaker"` |  |
 | speaker.image.tag | string | `nil` |  |
diff --git a/charts/metallb/templates/speaker.yaml b/charts/metallb/templates/speaker.yaml
index f7b33e25..f7e9dfe0 100644
--- a/charts/metallb/templates/speaker.yaml
+++ b/charts/metallb/templates/speaker.yaml
@@ -244,6 +244,9 @@ spec:
         {{- if .Values.speaker.wanConfig }}
         - --ml-wan-config
         {{- end }}
+        {{- if .Values.speaker.ignoreExcludeLB}}
+        - --ignore-exclude-lb
+        {{- end }}
         env:
         - name: METALLB_NODE_NAME
           valueFrom:
@@ -330,7 +333,7 @@ spec:
         {{- if or .Values.speaker.frr.enabled .Values.speaker.memberlist.enabled .Values.speaker.excludeInterfaces.enabled }}
         volumeMounts:
           {{- if .Values.speaker.memberlist.enabled }}
-          - name: memberlist 
+          - name: memberlist
             mountPath: {{ .Values.speaker.memberlist.mlSecretKeyPath }}
           {{- end }}
           {{- if .Values.speaker.frr.enabled }}
@@ -525,7 +528,7 @@ spec:
         operator: Exists
       - key: node-role.kubernetes.io/control-plane
         effect: NoSchedule
-        operator: Exists        
+        operator: Exists
       {{- end }}
       {{- with .Values.speaker.tolerations }}
         {{- toYaml . | nindent 6 }}
diff --git a/charts/metallb/values.schema.json b/charts/metallb/values.schema.json
index b6373532..bc0dd840 100644
--- a/charts/metallb/values.schema.json
+++ b/charts/metallb/values.schema.json
@@ -299,7 +299,7 @@
       },
       "required": [ "podMonitor", "prometheusRule" ]
     },
-    "controller": { 
+    "controller": {
       "allOf": [
         { "$ref": "#/definitions/component" },
         { "description": "MetalLB Controller",
@@ -330,7 +330,7 @@
         }
       ]
     },
-    "speaker": { 
+    "speaker": {
       "allOf": [
         { "$ref": "#/definitions/component" },
         { "description": "MetalLB Speaker",
@@ -364,6 +364,9 @@
                 }
               }
             },
+            "ignoreExcludeLB": {
+              "type": "boolean"
+            },
             "updateStrategy": {
               "type": "object",
               "properties": {
diff --git a/charts/metallb/values.yaml b/charts/metallb/values.yaml
index d9ec7989..738f25f8 100644
--- a/charts/metallb/values.yaml
+++ b/charts/metallb/values.yaml
@@ -268,6 +268,9 @@ speaker:
     mlSecretKeyPath: "/etc/ml_secret_key"
   excludeInterfaces:
     enabled: true
+  # ignore the exclude-from-external-loadbalancer label
+  ignoreExcludeLB: false
+
   image:
     repository: quay.io/metallb/speaker
     tag:
diff --git a/speaker/bgp_controller.go b/speaker/bgp_controller.go
index 314bee19..741664ab 100644
--- a/speaker/bgp_controller.go
+++ b/speaker/bgp_controller.go
@@ -51,13 +51,14 @@ type peer struct {
 }
 
 type bgpController struct {
-	logger         log.Logger
-	myNode         string
-	nodeLabels     labels.Set
-	peers          []*peer
-	svcAds         map[string][]*bgp.Advertisement
-	bgpType        bgpImplementation
-	sessionManager bgp.SessionManager
+	logger          log.Logger
+	myNode          string
+	nodeLabels      labels.Set
+	peers           []*peer
+	svcAds          map[string][]*bgp.Advertisement
+	bgpType         bgpImplementation
+	sessionManager  bgp.SessionManager
+	ignoreExcludeLB bool
 }
 
 func (c *bgpController) SetConfig(l log.Logger, cfg *config.Config) error {
@@ -157,7 +158,7 @@ func (c *bgpController) ShouldAnnounce(l log.Logger, name string, _ []net.IP, po
 		return "nodeNetworkUnavailable"
 	}
 
-	if k8snodes.IsNodeExcludedFromBalancers(nodes[c.myNode]) {
+	if !c.ignoreExcludeLB && k8snodes.IsNodeExcludedFromBalancers(nodes[c.myNode]) {
 		level.Debug(l).Log("event", "skipping should announce bgp", "service", name, "reason", "speaker's node has labeled 'node.kubernetes.io/exclude-from-external-load-balancers'")
 		return "nodeLabeledExcludeBalancers"
 	}
diff --git a/speaker/layer2_controller.go b/speaker/layer2_controller.go
index a572873c..778c875f 100644
--- a/speaker/layer2_controller.go
+++ b/speaker/layer2_controller.go
@@ -32,9 +32,10 @@ import (
 )
 
 type layer2Controller struct {
-	announcer *layer2.Announce
-	myNode    string
-	sList     SpeakerList
+	announcer       *layer2.Announce
+	myNode          string
+	ignoreExcludeLB bool
+	sList           SpeakerList
 }
 
 func (c *layer2Controller) SetConfig(log.Logger, *config.Config) error {
@@ -89,7 +90,7 @@ 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, nodes)
+	forPool := c.speakersForPool(pool, nodes)
 	var availableNodes []string
 	if svc.Spec.ExternalTrafficPolicy == v1.ServiceExternalTrafficPolicyTypeLocal {
 		availableNodes = usableNodes(eps, forPool)
@@ -205,14 +206,14 @@ func poolMatchesNodeL2(pool *config.Pool, node string) bool {
 	return false
 }
 
-func speakersForPool(speakers map[string]bool, pool *config.Pool, nodes map[string]*v1.Node) map[string]bool {
+func (c *layer2Controller) speakersForPool(pool *config.Pool, nodes map[string]*v1.Node) map[string]bool {
 	res := map[string]bool{}
-	for s := range speakers {
+	for s := range c.sList.UsableSpeakers() {
 		if k8snodes.IsNetworkUnavailable(nodes[s]) {
 			continue
 		}
 
-		if k8snodes.IsNodeExcludedFromBalancers(nodes[s]) {
+		if !c.ignoreExcludeLB && k8snodes.IsNodeExcludedFromBalancers(nodes[s]) {
 			continue
 		}
 
diff --git a/speaker/main.go b/speaker/main.go
index e10269a2..fd81f024 100644
--- a/speaker/main.go
+++ b/speaker/main.go
@@ -82,6 +82,7 @@ func main() {
 		logLevel          = flag.String("log-level", "info", fmt.Sprintf("log level. must be one of: [%s]", logging.Levels.String()))
 		enablePprof       = flag.Bool("enable-pprof", false, "Enable pprof profiling")
 		loadBalancerClass = flag.String("lb-class", "", "load balancer class. When enabled, metallb will handle only services whose spec.loadBalancerClass matches the given lb class")
+		ignoreLBExclude   = flag.Bool("ignore-exclude-lb", false, "ignore the exclude-from-external-load-balancers label")
 	)
 	flag.Parse()
 
@@ -157,6 +158,7 @@ func main() {
 		SList:                  sList,
 		bgpType:                bgpImplementation(bgpType),
 		InterfaceExcludeRegexp: interfacesToExclude,
+		IgnoreExcludeLB:        *ignoreLBExclude,
 	})
 	if err != nil {
 		level.Error(logger).Log("op", "startup", "error", err, "msg", "failed to create MetalLB controller")
@@ -240,16 +242,18 @@ type controllerConfig struct {
 	SupportedProtocols           []config.Proto
 	AnnouncedInterfacesToExclude []string `yaml:"announcedInterfacesToExclude"`
 	InterfaceExcludeRegexp       *regexp.Regexp
+	IgnoreExcludeLB              bool
 }
 
 func newController(cfg controllerConfig) (*controller, error) {
 	handlers := map[config.Proto]Protocol{
 		config.BGP: &bgpController{
-			logger:         cfg.Logger,
-			myNode:         cfg.MyNode,
-			svcAds:         make(map[string][]*bgp.Advertisement),
-			bgpType:        cfg.bgpType,
-			sessionManager: newBGP(cfg),
+			logger:          cfg.Logger,
+			myNode:          cfg.MyNode,
+			svcAds:          make(map[string][]*bgp.Advertisement),
+			bgpType:         cfg.bgpType,
+			sessionManager:  newBGP(cfg),
+			ignoreExcludeLB: cfg.IgnoreExcludeLB,
 		},
 	}
 	protocols := []config.Proto{config.BGP}
@@ -260,9 +264,10 @@ func newController(cfg controllerConfig) (*controller, error) {
 			return nil, fmt.Errorf("making layer2 announcer: %s", err)
 		}
 		handlers[config.Layer2] = &layer2Controller{
-			announcer: a,
-			myNode:    cfg.MyNode,
-			sList:     cfg.SList,
+			announcer:       a,
+			myNode:          cfg.MyNode,
+			sList:           cfg.SList,
+			ignoreExcludeLB: cfg.IgnoreExcludeLB,
 		}
 		protocols = append(protocols, config.Layer2)
 	}
diff --git a/website/content/release-notes/_index.md b/website/content/release-notes/_index.md
index a4707c81..4b169fbe 100644
--- a/website/content/release-notes/_index.md
+++ b/website/content/release-notes/_index.md
@@ -9,6 +9,9 @@ New features:
 
 - Bump FRR to 9.0.2 ([PR 2282](https://github.com/metallb/metallb/pull/2282), [Issue 2256](https://github.com/metallb/metallb/issues/2256))
 - Make the namse of webhook service and cert metallb specific to avoid name conflicts with other resources in the cluster. ([PR 2244](https://github.com/metallb/metallb/pull/2244), [Issue 2174](https://github.com/metallb/metallb/issues/2174)).
+New Features:
+
+- Provide a "ignore-exclude-lb" flag to ignore the `exclude-from-external-load-balancers` label ([PR 2280](https://github.com/metallb/metallb/pull/2280), [Issue 2274](https://github.com/metallb/metallb/issues/2274))
 
 BugFixes:
 
diff --git a/website/content/troubleshooting/_index.md b/website/content/troubleshooting/_index.md
index db9b2018..9f37781f 100644
--- a/website/content/troubleshooting/_index.md
+++ b/website/content/troubleshooting/_index.md
@@ -87,6 +87,13 @@ A given speaker won't advertise the service if:
 - there are no L2Advertisements / BGPAdvertisements matching the speaker node (if node selectors are specified)
 - the Kubernetes API reports "network not available" on the speaker's node
 
+## MetalLB is not advertising my service from my control-plane nodes or from my single node cluster
+
+Make sure your nodes are not labeled with the
+[node.kubernetes.io/exclude-from-external-load-balancers](https://kubernetes.io/docs/reference/labels-annotations-taints/#node-kubernetes-io-exclude-from-external-load-balancers) label.
+MetalLB honors that label and won't announce any service from such nodes. One way to circumvent
+the issue is to provide the speakers with the `--ignore-exclude-lb` flag (either from Helm or via Kustomize).
+
 ## MetalLB says it advertises the service but reaching the service does not work
 
 ### Checking the L2 advertisement works


Test patch:
diff --git a/speaker/bgp_controller_test.go b/speaker/bgp_controller_test.go
index db310c62..725b385a 100644
--- a/speaker/bgp_controller_test.go
+++ b/speaker/bgp_controller_test.go
@@ -6,6 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"net"
+	"os"
 	"sort"
 	"sync"
 	"testing"
@@ -1340,3 +1341,146 @@ func TestNodeSelectors(t *testing.T) {
 		}
 	}
 }
+
+func TestShouldAnnounceExcludeLB(t *testing.T) {
+	epsOn := func(node string) map[string][]discovery.EndpointSlice {
+		return map[string][]discovery.EndpointSlice{
+			"10.20.30.1": {
+				{
+					Endpoints: []discovery.Endpoint{
+						{
+							Addresses: []string{
+								"2.3.4.5",
+							},
+							NodeName: ptr.To(node),
+							Conditions: discovery.EndpointConditions{
+								Ready: ptr.To(true),
+							},
+						},
+					},
+				},
+			},
+		}
+	}
+
+	tests := []struct {
+		desc string
+
+		balancer            string
+		eps                 map[string][]discovery.EndpointSlice
+		trafficPolicy       v1.ServiceExternalTrafficPolicyType
+		excludeFromLB       []string
+		ignoreExcludeFromLB bool
+		c1ExpectedResult    map[string]string
+		c2ExpectedResult    map[string]string
+	}{
+		{
+			desc:          "One service, endpoint on iris1, no selector, both excluded, both should not announce",
+			balancer:      "test1",
+			eps:           epsOn("iris1"),
+			trafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster,
+			excludeFromLB: []string{"iris1", "iris2"},
+			c1ExpectedResult: map[string]string{
+				"10.20.30.1": "nodeLabeledExcludeBalancers",
+			},
+			c2ExpectedResult: map[string]string{
+				"10.20.30.1": "nodeLabeledExcludeBalancers",
+			},
+		},
+		{
+			desc:          "One service, endpoint on iris1, no selector, etplocal, ignore excludelb, both should announce",
+			balancer:      "test1",
+			eps:           epsOn("iris1"),
+			trafficPolicy: v1.ServiceExternalTrafficPolicyTypeCluster,
+			excludeFromLB: []string{"iris1", "iris2"},
+			c1ExpectedResult: map[string]string{
+				"10.20.30.1": "",
+			},
+			c2ExpectedResult: map[string]string{
+				"10.20.30.1": "",
+			},
+			ignoreExcludeFromLB: true,
+		},
+	}
+	l := log.NewNopLogger()
+	for _, test := range tests {
+		cfg := config.Config{
+			Pools: &config.Pools{ByName: map[string]*config.Pool{
+				"default": {
+					CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
+					BGPAdvertisements: []*config.BGPAdvertisement{{
+						Nodes: map[string]bool{
+							"iris1": true,
+							"iris2": true,
+						},
+					}},
+				},
+			}},
+		}
+		c1, err := newController(controllerConfig{
+			MyNode:          "iris1",
+			Logger:          log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)),
+			bgpType:         bgpNative,
+			IgnoreExcludeLB: test.ignoreExcludeFromLB,
+		})
+		if err != nil {
+			t.Fatalf("creating controller: %s", err)
+		}
+		c1.client = &testK8S{t: t}
+
+		c2, err := newController(controllerConfig{
+			MyNode:          "iris2",
+			Logger:          log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)),
+			bgpType:         bgpNative,
+			IgnoreExcludeLB: test.ignoreExcludeFromLB,
+		})
+		if err != nil {
+			t.Fatalf("creating controller: %s", err)
+		}
+		c2.client = &testK8S{t: t}
+
+		if c1.SetConfig(l, &cfg) == controllers.SyncStateError {
+			t.Errorf("%q: SetConfig failed", test.desc)
+		}
+		if c2.SetConfig(l, &cfg) == controllers.SyncStateError {
+			t.Errorf("%q: SetConfig failed", test.desc)
+		}
+		svc := v1.Service{
+			Spec: v1.ServiceSpec{
+				Type:                  "LoadBalancer",
+				ExternalTrafficPolicy: test.trafficPolicy,
+			},
+			Status: statusAssigned("10.20.30.1"),
+		}
+
+		lbIP := net.ParseIP(svc.Status.LoadBalancer.Ingress[0].IP)
+		lbIPStr := lbIP.String()
+
+		nodes := map[string]*v1.Node{
+			"iris1": {
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "iris1",
+				},
+			},
+			"iris2": {
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "iris2",
+				},
+			},
+		}
+		for _, n := range test.excludeFromLB {
+			nodes[n].Labels = map[string]string{
+				v1.LabelNodeExcludeBalancers: "",
+			}
+		}
+
+		response1 := c1.protocolHandlers[config.BGP].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], &svc, test.eps[lbIPStr], nodes)
+		response2 := c2.protocolHandlers[config.BGP].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], &svc, test.eps[lbIPStr], nodes)
+		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)
+		}
+		if response2 != test.c2ExpectedResult[lbIPStr] {
+			t.Errorf("%q: shouldAnnounce for controller 2 for service %s returned incorrect result, expected '%s', but received '%s'", test.desc, lbIPStr, test.c2ExpectedResult[lbIPStr], response2)
+		}
+	}
+}
diff --git a/speaker/layer2_controller_test.go b/speaker/layer2_controller_test.go
index 9f42da54..ad640ce6 100644
--- a/speaker/layer2_controller_test.go
+++ b/speaker/layer2_controller_test.go
@@ -7,7 +7,6 @@ import (
 	"net"
 	"os"
 	"sort"
-	"strings"
 	"testing"
 
 	"go.universe.tf/metallb/internal/config"
@@ -1122,34 +1121,13 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 	}
 }
 
-func TestShouldAnnounceNodeSelector(t *testing.T) {
+func TestShouldAnnounceFromNodes(t *testing.T) {
 	fakeSL := &fakeSpeakerList{
 		speakers: map[string]bool{
 			"iris1": true,
 			"iris2": true,
 		},
 	}
-	c1, err := newController(controllerConfig{
-		MyNode:  "iris1",
-		Logger:  log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)),
-		SList:   fakeSL,
-		bgpType: bgpNative,
-	})
-	if err != nil {
-		t.Fatalf("creating controller: %s", err)
-	}
-	c1.client = &testK8S{t: t}
-
-	c2, err := newController(controllerConfig{
-		MyNode:  "iris2",
-		Logger:  log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)),
-		SList:   fakeSL,
-		bgpType: bgpNative,
-	})
-	if err != nil {
-		t.Fatalf("creating controller: %s", err)
-	}
-	c2.client = &testK8S{t: t}
 	advertisementsForBoth := []*config.L2Advertisement{
 		{
 			Nodes: map[string]bool{
@@ -1238,12 +1216,14 @@ func TestShouldAnnounceNodeSelector(t *testing.T) {
 	tests := []struct {
 		desc string
 
-		balancer         string
-		L2Advertisements []*config.L2Advertisement
-		eps              map[string][]discovery.EndpointSlice
-		trafficPolicy    v1.ServiceExternalTrafficPolicyType
-		c1ExpectedResult map[string]string
-		c2ExpectedResult map[string]string
+		balancer            string
+		L2Advertisements    []*config.L2Advertisement
+		eps                 map[string][]discovery.EndpointSlice
+		trafficPolicy       v1.ServiceExternalTrafficPolicyType
+		excludeFromLB       []string
+		ignoreExcludeFromLB bool
+		c1ExpectedResult    map[string]string
+		c2ExpectedResult    map[string]string
 	}{
 		{
 			desc:             "One service, endpoint on iris1, selector on iris1, c1 should announce",
@@ -1261,7 +1241,7 @@ func TestShouldAnnounceNodeSelector(t *testing.T) {
 		{
 			desc:             "One service, etp local, endpoint on iris1, selector on both(split), c2 should announce",
 			balancer:         "test1",
-			eps:              epsOn("iris2"),
+			eps:              epsOn("iris1"),
 			L2Advertisements: advertisementSplit,
 			trafficPolicy:    v1.ServiceExternalTrafficPolicyTypeLocal,
 			c1ExpectedResult: map[string]string{
@@ -1310,13 +1290,66 @@ func TestShouldAnnounceNodeSelector(t *testing.T) {
 				"10.20.30.1": "notOwner",
 			},
 		},
+		{
+			desc:             "One service, endpoint on iris1, no selector, iris2 excluded, c1 should announce",
+			balancer:         "test1",
+			eps:              epsOn("iris1"),
+			L2Advertisements: advertisementsForBoth,
+			trafficPolicy:    v1.ServiceExternalTrafficPolicyTypeCluster,
+			excludeFromLB:    []string{"iris2"},
+			c1ExpectedResult: map[string]string{
+				"10.20.30.1": "",
+			},
+			c2ExpectedResult: map[string]string{
+				"10.20.30.1": "notOwner",
+			},
+		},
+		{
+			desc:             "One service, endpoint on iris1, no selector, iris1 excluded, c2 should announce",
+			balancer:         "test1",
+			eps:              epsOn("iris1"),
+			L2Advertisements: advertisementsForBoth,
+			trafficPolicy:    v1.ServiceExternalTrafficPolicyTypeCluster,
+			excludeFromLB:    []string{"iris1"},
+			c1ExpectedResult: map[string]string{
+				"10.20.30.1": "notOwner",
+			},
+			c2ExpectedResult: map[string]string{
+				"10.20.30.1": "",
+			},
+		},
+		{
+			desc:             "One service, endpoint on iris1, no selector, both excluded, both should not announce",
+			balancer:         "test1",
+			eps:              epsOn("iris1"),
+			L2Advertisements: advertisementsForBoth,
+			trafficPolicy:    v1.ServiceExternalTrafficPolicyTypeCluster,
+			excludeFromLB:    []string{"iris1", "iris2"},
+			c1ExpectedResult: map[string]string{
+				"10.20.30.1": "notOwner",
+			},
+			c2ExpectedResult: map[string]string{
+				"10.20.30.1": "notOwner",
+			},
+		},
+		{
+			desc:             "One service, endpoint on iris1, no selector, etplocal, both excluded, ignore excludelb, c1 should announce",
+			balancer:         "test1",
+			eps:              epsOn("iris1"),
+			L2Advertisements: advertisementsForBoth,
+			trafficPolicy:    v1.ServiceExternalTrafficPolicyTypeLocal,
+			excludeFromLB:    []string{"iris1", "iris2"},
+			c1ExpectedResult: map[string]string{
+				"10.20.30.1": "",
+			},
+			c2ExpectedResult: map[string]string{
+				"10.20.30.1": "notOwner",
+			},
+			ignoreExcludeFromLB: true,
+		},
 	}
 	l := log.NewNopLogger()
 	for _, test := range tests {
-		if !strings.Contains(test.desc, "ltp") {
-			continue
-		}
-
 		cfg := config.Config{
 			Pools: &config.Pools{ByName: map[string]*config.Pool{
 				"default": {
@@ -1325,6 +1358,30 @@ func TestShouldAnnounceNodeSelector(t *testing.T) {
 				},
 			}},
 		}
+		c1, err := newController(controllerConfig{
+			MyNode:          "iris1",
+			Logger:          log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)),
+			SList:           fakeSL,
+			bgpType:         bgpNative,
+			IgnoreExcludeLB: test.ignoreExcludeFromLB,
+		})
+		if err != nil {
+			t.Fatalf("creating controller: %s", err)
+		}
+		c1.client = &testK8S{t: t}
+
+		c2, err := newController(controllerConfig{
+			MyNode:          "iris2",
+			Logger:          log.NewLogfmtLogger(log.NewSyncWriter(os.Stderr)),
+			SList:           fakeSL,
+			bgpType:         bgpNative,
+			IgnoreExcludeLB: test.ignoreExcludeFromLB,
+		})
+		if err != nil {
+			t.Fatalf("creating controller: %s", err)
+		}
+		c2.client = &testK8S{t: t}
+
 		if c1.SetConfig(l, &cfg) == controllers.SyncStateError {
 			t.Errorf("%q: SetConfig failed", test.desc)
 		}
@@ -1341,8 +1398,27 @@ 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], nil)
-		response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], &svc, test.eps[lbIPStr], nil)
+
+		nodes := map[string]*v1.Node{
+			"iris1": {
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "iris1",
+				},
+			},
+			"iris2": {
+				ObjectMeta: metav1.ObjectMeta{
+					Name: "iris2",
+				},
+			},
+		}
+		for _, n := range test.excludeFromLB {
+			nodes[n].Labels = map[string]string{
+				v1.LabelNodeExcludeBalancers: "",
+			}
+		}
+
+		response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], &svc, test.eps[lbIPStr], nodes)
+		response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], &svc, test.eps[lbIPStr], nodes)
 		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)
 		}