Repository: metallb/metallb
Pull request #1693 (2022-11-08 12:58:43)

Implement ip pool selector logic for service
This changes ip allocation logic when ip pools are configured with namespace, service/namespace label selectors so that ip are allocated from desired pools based on its priority before it tries to allocate from other ip pools.

Signed-off-by: Periyasamy Palanisamy <pepalani@redhat.com>

yuvalk: hint for static-security-analysis,
see my fixes for older code here:
https://github.com/metallb/metallb/commit/b3e8e242da9cbb018e9b16cbd5392b866c146d52
and a nice explanation here:
https://medium.com/swlh/use-pointer-of-for-range-loop-variable-in-go-3d3481f7ffc9
pperiyasamy: > hint for static-security-analysis,

Can  you also make this available from inv command via [tasks.py](https://github.com/metallb/metallb/blob/main/tasks.py) ?
fedepaol: LGTM!

Patch:
diff --git a/charts/metallb/templates/rbac.yaml b/charts/metallb/templates/rbac.yaml
index 86f86086..50d41f50 100644
--- a/charts/metallb/templates/rbac.yaml
+++ b/charts/metallb/templates/rbac.yaml
@@ -7,7 +7,7 @@ metadata:
     {{- include "metallb.labels" . | nindent 4 }}
 rules:
 - apiGroups: [""]
-  resources: ["services"]
+  resources: ["services", "namespaces"]
   verbs: ["get", "list", "watch"]
 - apiGroups: [""]
   resources: ["services/status"]
@@ -47,7 +47,7 @@ metadata:
     {{- include "metallb.labels" . | nindent 4 }}
 rules:
 - apiGroups: [""]
-  resources: ["services", "endpoints", "nodes"]
+  resources: ["services", "endpoints", "nodes", "namespaces"]
   verbs: ["get", "list", "watch"]
 - apiGroups: ["discovery.k8s.io"]
   resources: ["endpointslices"]
diff --git a/config/manifests/metallb-frr-prometheus.yaml b/config/manifests/metallb-frr-prometheus.yaml
index 2e0bfa91..e2d34d60 100644
--- a/config/manifests/metallb-frr-prometheus.yaml
+++ b/config/manifests/metallb-frr-prometheus.yaml
@@ -1541,6 +1541,7 @@ rules:
   - ""
   resources:
   - services
+  - namespaces
   verbs:
   - get
   - list
@@ -1640,6 +1641,7 @@ rules:
   - services
   - endpoints
   - nodes
+  - namespaces
   verbs:
   - get
   - list
diff --git a/config/manifests/metallb-frr.yaml b/config/manifests/metallb-frr.yaml
index 1aa28dcc..b59ba683 100644
--- a/config/manifests/metallb-frr.yaml
+++ b/config/manifests/metallb-frr.yaml
@@ -1512,6 +1512,7 @@ rules:
   - ""
   resources:
   - services
+  - namespaces
   verbs:
   - get
   - list
@@ -1599,6 +1600,7 @@ rules:
   - services
   - endpoints
   - nodes
+  - namespaces
   verbs:
   - get
   - list
diff --git a/config/manifests/metallb-native-prometheus.yaml b/config/manifests/metallb-native-prometheus.yaml
index d0ddabe4..f03e72e0 100644
--- a/config/manifests/metallb-native-prometheus.yaml
+++ b/config/manifests/metallb-native-prometheus.yaml
@@ -1541,6 +1541,7 @@ rules:
   - ""
   resources:
   - services
+  - namespaces
   verbs:
   - get
   - list
@@ -1640,6 +1641,7 @@ rules:
   - services
   - endpoints
   - nodes
+  - namespaces
   verbs:
   - get
   - list
diff --git a/config/manifests/metallb-native.yaml b/config/manifests/metallb-native.yaml
index ca9e50db..780dcc36 100644
--- a/config/manifests/metallb-native.yaml
+++ b/config/manifests/metallb-native.yaml
@@ -1512,6 +1512,7 @@ rules:
   - ""
   resources:
   - services
+  - namespaces
   verbs:
   - get
   - list
@@ -1599,6 +1600,7 @@ rules:
   - services
   - endpoints
   - nodes
+  - namespaces
   verbs:
   - get
   - list
diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml
index 6ce0c181..043cbdef 100644
--- a/config/rbac/role.yaml
+++ b/config/rbac/role.yaml
@@ -9,6 +9,7 @@ rules:
       - ""
     resources:
       - services
+      - namespaces
     verbs:
       - get
       - list
@@ -96,6 +97,7 @@ rules:
       - services
       - endpoints
       - nodes
+      - namespaces
     verbs:
       - get
       - list
diff --git a/controller/main.go b/controller/main.go
index 62cb5c6d..6c16defe 100644
--- a/controller/main.go
+++ b/controller/main.go
@@ -42,7 +42,7 @@ type service interface {
 
 type controller struct {
 	client service
-	pools  map[string]*config.Pool
+	pools  *config.Pools
 	ips    *allocator.Allocator
 }
 
@@ -58,7 +58,7 @@ func (c *controller) SetBalancer(l log.Logger, name string, svcRo *v1.Service, _
 		return controllers.SyncStateReprocessAll
 	}
 
-	if c.pools == nil {
+	if c.pools == nil || c.pools.ByName == nil {
 		// Config hasn't been read, nothing we can do just yet.
 		level.Debug(l).Log("event", "noConfig", "msg", "not processing, still waiting for config")
 		return controllers.SyncStateSuccess
@@ -112,11 +112,11 @@ func (c *controller) deleteBalancer(l log.Logger, name string) {
 	}
 }
 
-func (c *controller) SetPools(l log.Logger, pools map[string]*config.Pool) controllers.SyncState {
+func (c *controller) SetPools(l log.Logger, pools *config.Pools) controllers.SyncState {
 	level.Debug(l).Log("event", "startUpdate", "msg", "start of config update")
 	defer level.Debug(l).Log("event", "endUpdate", "msg", "end of config update")
 
-	if pools == nil {
+	if pools == nil || pools.ByName == nil {
 		level.Error(l).Log("op", "setConfig", "error", "no MetalLB configuration in cluster", "msg", "configuration is missing, MetalLB will not function")
 		return controllers.SyncStateErrorNoRetry
 	}
diff --git a/controller/service.go b/controller/service.go
index e00fbded..70707cff 100644
--- a/controller/service.go
+++ b/controller/service.go
@@ -148,7 +148,7 @@ func (c *controller) convergeBalancer(l log.Logger, key string, svc *v1.Service)
 	}
 
 	pool := c.ips.Pool(key)
-	if pool == "" || c.pools[pool] == nil {
+	if pool == "" || c.pools == nil || c.pools.IsEmpty(pool) {
 		level.Error(l).Log("bug", "true", "ip", lbIPs, "msg", "internal error: allocated IP has no matching address pool")
 		c.client.Errorf(svc, "InternalError", "allocated an IP that has no pool")
 		c.clearServiceState(key, svc)
@@ -214,7 +214,7 @@ func (c *controller) allocateIPs(key string, svc *v1.Service) ([]net.IP, error)
 	}
 
 	// Okay, in that case just bruteforce across all pools.
-	return c.ips.Allocate(key, serviceIPFamily, k8salloc.Ports(svc), k8salloc.SharingKey(svc), k8salloc.BackendKey(svc))
+	return c.ips.Allocate(key, svc, serviceIPFamily, k8salloc.Ports(svc), k8salloc.SharingKey(svc), k8salloc.BackendKey(svc))
 }
 
 func (c *controller) isServiceAllocated(key string) bool {
diff --git a/internal/allocator/allocator.go b/internal/allocator/allocator.go
index 637af5b2..3fd80de6 100644
--- a/internal/allocator/allocator.go
+++ b/internal/allocator/allocator.go
@@ -7,17 +7,20 @@ import (
 	"fmt"
 	"math"
 	"net"
+	"sort"
 	"strings"
 
 	"go.universe.tf/metallb/internal/config"
 	"go.universe.tf/metallb/internal/ipfamily"
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/labels"
 
 	"github.com/mikioh/ipaddr"
 )
 
 // An Allocator tracks IP address pools and allocates addresses from them.
 type Allocator struct {
-	pools map[string]*config.Pool
+	pools *config.Pools
 
 	allocated       map[string]*alloc          // svc -> alloc
 	sharingKeyForIP map[string]*key            // ip.String() -> assigned sharing key
@@ -52,7 +55,7 @@ type alloc struct {
 // New returns an Allocator managing no pools.
 func New() *Allocator {
 	return &Allocator{
-		pools: map[string]*config.Pool{},
+		pools: &config.Pools{ByName: map[string]*config.Pool{}},
 
 		allocated:       map[string]*alloc{},
 		sharingKeyForIP: map[string]*key{},
@@ -63,19 +66,19 @@ func New() *Allocator {
 }
 
 // SetPools updates the set of address pools that the allocator owns.
-func (a *Allocator) SetPools(pools map[string]*config.Pool) error {
+func (a *Allocator) SetPools(pools *config.Pools) error {
 	// All the fancy sharing stuff only influences how new allocations
 	// can be created. For changing the underlying configuration, the
 	// only question we have to answer is: can we fit all allocated
 	// IPs into address pools under the new configuration?
 	for svc, alloc := range a.allocated {
-		if poolFor(pools, alloc.ips) == "" {
+		if poolFor(pools.ByName, alloc.ips) == "" {
 			return fmt.Errorf("new config not compatible with assigned IPs: service %q cannot own %q under new config", svc, alloc.ips)
 		}
 	}
 
-	for n := range a.pools {
-		if pools[n] == nil {
+	for n := range a.pools.ByName {
+		if pools.ByName[n] == nil {
 			stats.poolCapacity.DeleteLabelValues(n)
 			stats.poolActive.DeleteLabelValues(n)
 			stats.poolAllocated.DeleteLabelValues(n)
@@ -86,7 +89,7 @@ func (a *Allocator) SetPools(pools map[string]*config.Pool) error {
 
 	// Need to rearrange existing pool mappings and counts
 	for svc, alloc := range a.allocated {
-		pool := poolFor(a.pools, alloc.ips)
+		pool := poolFor(a.pools.ByName, alloc.ips)
 		if pool != alloc.pool {
 			a.Unassign(svc)
 			alloc.pool = pool
@@ -97,7 +100,7 @@ func (a *Allocator) SetPools(pools map[string]*config.Pool) error {
 	}
 
 	// Refresh or initiate stats
-	for n, p := range a.pools {
+	for n, p := range a.pools.ByName {
 		stats.poolCapacity.WithLabelValues(n).Set(float64(poolCount(p)))
 		stats.poolActive.WithLabelValues(n).Set(float64(len(a.poolIPsInUse[n])))
 	}
@@ -127,14 +130,14 @@ func (a *Allocator) assign(svc string, alloc *alloc) {
 		}
 		a.poolIPsInUse[alloc.pool][ip.String()]++
 	}
-	stats.poolCapacity.WithLabelValues(alloc.pool).Set(float64(poolCount(a.pools[alloc.pool])))
+	stats.poolCapacity.WithLabelValues(alloc.pool).Set(float64(poolCount(a.pools.ByName[alloc.pool])))
 	stats.poolActive.WithLabelValues(alloc.pool).Set(float64(len(a.poolIPsInUse[alloc.pool])))
 }
 
 // Assign assigns the requested ip to svc, if the assignment is
 // permissible by sharingKey and backendKey.
 func (a *Allocator) Assign(svc string, ips []net.IP, ports []Port, sharingKey, backendKey string) error {
-	pool := poolFor(a.pools, ips)
+	pool := poolFor(a.pools.ByName, ips)
 	if pool == "" {
 		return fmt.Errorf("%q is not allowed in config", ips)
 	}
@@ -229,7 +232,7 @@ func (a *Allocator) AllocateFromPool(svc string, serviceIPFamily ipfamily.Family
 		return alloc.ips, nil
 	}
 
-	pool := a.pools[poolName]
+	pool := a.pools.ByName[poolName]
 	if pool == nil {
 		return nil, fmt.Errorf("unknown pool %q", poolName)
 	}
@@ -269,19 +272,24 @@ func (a *Allocator) AllocateFromPool(svc string, serviceIPFamily ipfamily.Family
 }
 
 // Allocate assigns any available and assignable IP to service.
-func (a *Allocator) Allocate(svc string, serviceIPFamily ipfamily.Family, ports []Port, sharingKey, backendKey string) ([]net.IP, error) {
-	if alloc := a.allocated[svc]; alloc != nil {
-		if err := a.Assign(svc, alloc.ips, ports, sharingKey, backendKey); err != nil {
+func (a *Allocator) Allocate(svcKey string, svc *v1.Service, serviceIPFamily ipfamily.Family, ports []Port, sharingKey, backendKey string) ([]net.IP, error) {
+	if alloc := a.allocated[svcKey]; alloc != nil {
+		if err := a.Assign(svcKey, alloc.ips, ports, sharingKey, backendKey); err != nil {
 			return nil, err
 		}
 		return alloc.ips, nil
 	}
-
-	for poolName := range a.pools {
-		if !a.pools[poolName].AutoAssign {
+	pinnedPools := a.sortedPoolsForService(svc)
+	for _, pool := range pinnedPools {
+		if ips, err := a.AllocateFromPool(svcKey, serviceIPFamily, pool.Name, ports, sharingKey, backendKey); err == nil {
+			return ips, nil
+		}
+	}
+	for _, pool := range a.pools.ByName {
+		if !pool.AutoAssign || pool.ServiceAllocations != nil {
 			continue
 		}
-		if ips, err := a.AllocateFromPool(svc, serviceIPFamily, poolName, ports, sharingKey, backendKey); err == nil {
+		if ips, err := a.AllocateFromPool(svcKey, serviceIPFamily, pool.Name, ports, sharingKey, backendKey); err == nil {
 			return ips, nil
 		}
 	}
@@ -289,11 +297,54 @@ func (a *Allocator) Allocate(svc string, serviceIPFamily ipfamily.Family, ports
 	return nil, errors.New("no available IPs")
 }
 
+// This method returns sorted ip pools which are allocatable for given service.
+// When ip pool is not set with priority, then just append it after sorted
+// priority ip pools.
+func (a *Allocator) sortedPoolsForService(svc *v1.Service) []*config.Pool {
+	var priorityPools, noPriorityPools []*config.Pool
+	if svc == nil {
+		return priorityPools
+	}
+	for _, nsPoolName := range a.pools.ByNamespace[svc.Namespace] {
+		if nsPool, ok := a.pools.ByName[nsPoolName]; ok {
+			if !nsPool.AutoAssign {
+				continue
+			}
+			if nsPool.ServiceAllocations.Priority > 0 {
+				priorityPools = append(priorityPools, nsPool)
+				continue
+			}
+			noPriorityPools = append(noPriorityPools, nsPool)
+		}
+	}
+	svcLabels := labels.Set(svc.Labels)
+	for _, svcPoolName := range a.pools.ByServiceSelector {
+		if svcPool, ok := a.pools.ByName[svcPoolName]; ok {
+			if !svcPool.AutoAssign {
+				continue
+			}
+			for _, svcSelector := range svcPool.ServiceAllocations.ServiceSelectors {
+				isMatch := svcSelector.Matches(svcLabels)
+				if isMatch && svcPool.ServiceAllocations.Priority > 0 {
+					priorityPools = append(priorityPools, svcPool)
+				} else if isMatch {
+					noPriorityPools = append(noPriorityPools, svcPool)
+				}
+			}
+		}
+	}
+	sort.Slice(priorityPools, func(i, j int) bool {
+		return priorityPools[i].ServiceAllocations.Priority <
+			priorityPools[j].ServiceAllocations.Priority
+	})
+	return append(priorityPools, noPriorityPools...)
+}
+
 // Pool returns the pool from which service's IP was allocated. If
 // service has no IP allocated, "" is returned.
 func (a *Allocator) Pool(svc string) string {
 	if alloc := a.allocated[svc]; alloc != nil {
-		return poolFor(a.pools, alloc.ips)
+		return poolFor(a.pools.ByName, alloc.ips)
 	}
 	return ""
 }
diff --git a/internal/config/config.go b/internal/config/config.go
index 21fc084f..01643949 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -44,6 +44,7 @@ type ClusterResources struct {
 	Communities        []metallbv1beta1.Community        `json:"communities"`
 	PasswordSecrets    map[string]corev1.Secret          `json:"passwordsecrets"`
 	Nodes              []corev1.Node                     `json:"nodes"`
+	Namespaces         []corev1.Namespace                `json:"namespaces"`
 }
 
 // Config is a parsed MetalLB configuration.
@@ -51,11 +52,21 @@ type Config struct {
 	// Routers that MetalLB should peer with.
 	Peers []*Peer
 	// Address pools from which to allocate load balancer IPs.
-	Pools map[string]*Pool
+	Pools *Pools
 	// BFD profiles that can be used by peers.
 	BFDProfiles map[string]*BFDProfile
 }
 
+// Pools contains address pools and its namespace/service specific allocations.
+type Pools struct {
+	// ByName a map containing all configured pools.
+	ByName map[string]*Pool
+	// ByNamespace contains pool names pinned to specific namespace.
+	ByNamespace map[string][]string
+	// ByServiceSelector contains pool names which has service selection labels.
+	ByServiceSelector []string
+}
+
 // Proto holds the protocol we are speaking.
 type Proto string
 
@@ -105,6 +116,8 @@ type Peer struct {
 
 // Pool is the configuration of an IP address pool.
 type Pool struct {
+	// Pool Name
+	Name string
 	// The addresses that are part of this pool, expressed as CIDR
 	// prefixes. config.Parse guarantees that these are
 	// non-overlapping, both within and between pools.
@@ -126,6 +139,19 @@ type Pool struct {
 	L2Advertisements []*L2Advertisement
 
 	cidrsPerAddresses map[string][]*net.IPNet
+
+	ServiceAllocations *ServiceAllocation
+}
+
+// ServiceAllocation makes ip pool allocation to specific namespace and/or service.
+type ServiceAllocation struct {
+	// The priority of ip pool for a given service allocation.
+	Priority int
+	// Set of namespaces on which ip pool can be attached.
+	Namespaces sets.String
+	// Service selectors to select service for which ip pool can be used
+	// for ip allocation.
+	ServiceSelectors []labels.Selector
 }
 
 // BGPAdvertisement describes one translation from an IP address to a BGP advertisement.
@@ -170,6 +196,10 @@ type BFDProfile struct {
 	MinimumTTL       *uint32
 }
 
+func (p *Pools) IsEmpty(pool string) bool {
+	return p.ByName[pool] == nil
+}
+
 // Parse loads and validates a Config from bs.
 func For(resources ClusterResources, validate Validate) (*Config, error) {
 	err := validate(resources)
@@ -237,8 +267,8 @@ func peersFor(resources ClusterResources, BFDProfiles map[string]*BFDProfile) ([
 	return res, nil
 }
 
-func poolsFor(resources ClusterResources) (map[string]*Pool, error) {
-	res := make(map[string]*Pool)
+func poolsFor(resources ClusterResources) (*Pools, error) {
+	pools := make(map[string]*Pool)
 	communities, err := communitiesFromCrs(resources.Communities)
 	if err != nil {
 		return nil, err
@@ -246,13 +276,13 @@ func poolsFor(resources ClusterResources) (map[string]*Pool, error) {
 
 	var allCIDRs []*net.IPNet
 	for _, p := range resources.Pools {
-		pool, err := addressPoolFromCR(p)
+		pool, err := addressPoolFromCR(p, resources.Namespaces)
 		if err != nil {
 			return nil, fmt.Errorf("parsing address pool %s: %s", p.Name, err)
 		}
 
 		// Check that the pool isn't already defined
-		if res[p.Name] != nil {
+		if pools[p.Name] != nil {
 			return nil, fmt.Errorf("duplicate definition of pool %q", p.Name)
 		}
 
@@ -266,10 +296,10 @@ func poolsFor(resources ClusterResources) (map[string]*Pool, error) {
 			allCIDRs = append(allCIDRs, cidr)
 		}
 
-		res[p.Name] = pool
+		pools[p.Name] = pool
 	}
 
-	err = setL2AdvertisementsToPools(resources.Pools, resources.L2Advs, resources.Nodes, res)
+	err = setL2AdvertisementsToPools(resources.Pools, resources.L2Advs, resources.Nodes, pools)
 	if err != nil {
 		return nil, err
 	}
@@ -279,7 +309,7 @@ func poolsFor(resources ClusterResources) (map[string]*Pool, error) {
 		return nil, err
 	}
 
-	err = setBGPAdvertisementsToPools(resources.Pools, resources.BGPAdvs, resources.Nodes, res, communities)
+	err = setBGPAdvertisementsToPools(resources.Pools, resources.BGPAdvs, resources.Nodes, pools, communities)
 	if err != nil {
 		return nil, err
 	}
@@ -295,7 +325,7 @@ func poolsFor(resources ClusterResources) (map[string]*Pool, error) {
 		}
 
 		// Check that the pool isn't already defined
-		if res[p.Name] != nil {
+		if pools[p.Name] != nil {
 			return nil, fmt.Errorf("duplicate definition of pool %q", p.Name)
 		}
 
@@ -309,10 +339,10 @@ func poolsFor(resources ClusterResources) (map[string]*Pool, error) {
 			allCIDRs = append(allCIDRs, cidr)
 		}
 
-		res[p.Name] = pool
+		pools[p.Name] = pool
 	}
-
-	return res, nil
+	return &Pools{ByName: pools, ByNamespace: poolsByNamespace(pools),
+		ByServiceSelector: poolsByServiceSelector(pools)}, nil
 }
 
 func communitiesFromCrs(cs []metallbv1beta1.Community) (map[string]uint32, error) {
@@ -446,12 +476,13 @@ func passwordForPeer(p metallbv1beta2.BGPPeer, passwordSecrets map[string]corev1
 	return password, nil
 }
 
-func addressPoolFromCR(p metallbv1beta1.IPAddressPool) (*Pool, error) {
+func addressPoolFromCR(p metallbv1beta1.IPAddressPool, namespaces []corev1.Namespace) (*Pool, error) {
 	if p.Name == "" {
 		return nil, errors.New("missing pool name")
 	}
 
 	ret := &Pool{
+		Name:          p.Name,
 		AvoidBuggyIPs: p.Spec.AvoidBuggyIPs,
 		AutoAssign:    true,
 	}
@@ -474,15 +505,78 @@ func addressPoolFromCR(p metallbv1beta1.IPAddressPool) (*Pool, error) {
 		ret.cidrsPerAddresses[cidr] = nets
 	}
 
+	serviceAllocations, err := addressPoolServiceAllocationsFromCR(p, namespaces)
+	if err != nil {
+		return nil, err
+	}
+	ret.ServiceAllocations = serviceAllocations
+
 	return ret, nil
 }
 
+func addressPoolServiceAllocationsFromCR(p metallbv1beta1.IPAddressPool, namespaces []corev1.Namespace) (*ServiceAllocation, error) {
+	if p.Spec.AllocateTo == nil {
+		return nil, nil
+	}
+	serviceAllocations := &ServiceAllocation{Priority: p.Spec.AllocateTo.Priority,
+		Namespaces: sets.NewString(p.Spec.AllocateTo.Namespaces...)}
+	for i := range p.Spec.AllocateTo.NamespaceSelectors {
+		l, err := metav1.LabelSelectorAsSelector(&p.Spec.AllocateTo.NamespaceSelectors[i])
+		if err != nil {
+			return nil, errors.Wrapf(err, "invalid namespace label selector %v in ip pool %s", &p.Spec.AllocateTo.NamespaceSelectors[i], p.Name)
+		}
+		for _, ns := range namespaces {
+			nsLabels := labels.Set(ns.Labels)
+			if l.Matches(nsLabels) {
+				serviceAllocations.Namespaces.Insert(ns.Name)
+			}
+		}
+	}
+	for i := range p.Spec.AllocateTo.ServiceSelectors {
+		l, err := metav1.LabelSelectorAsSelector(&p.Spec.AllocateTo.ServiceSelectors[i])
+		if err != nil {
+			return nil, errors.Wrapf(err, "invalid service label selector %v in ip pool %s", p.Spec.AllocateTo.ServiceSelectors[i], p.Name)
+		}
+		serviceAllocations.ServiceSelectors = append(serviceAllocations.ServiceSelectors, l)
+	}
+	return serviceAllocations, nil
+}
+
+func poolsByNamespace(pools map[string]*Pool) map[string][]string {
+	var poolsForNamespace map[string][]string
+	for _, pool := range pools {
+		if pool.ServiceAllocations == nil {
+			continue
+		}
+		if poolsForNamespace == nil && len(pool.ServiceAllocations.Namespaces) > 0 {
+			poolsForNamespace = make(map[string][]string)
+		}
+		for namespace := range pool.ServiceAllocations.Namespaces {
+			poolsForNamespace[namespace] = append(poolsForNamespace[namespace], pool.Name)
+		}
+	}
+	return poolsForNamespace
+}
+
+func poolsByServiceSelector(pools map[string]*Pool) []string {
+	var poolsByServiceSelector []string
+	for _, pool := range pools {
+		if pool.ServiceAllocations == nil || len(pool.ServiceAllocations.ServiceSelectors) == 0 {
+			continue
+		}
+		poolsByServiceSelector = append(poolsByServiceSelector, pool.Name)
+	}
+	sort.Strings(poolsByServiceSelector)
+	return poolsByServiceSelector
+}
+
 func addressPoolFromLegacyCR(p metallbv1beta1.AddressPool, bgpCommunities map[string]uint32, allNodes map[string]bool) (*Pool, error) {
 	if p.Name == "" {
 		return nil, errors.New("missing pool name")
 	}
 
 	ret := &Pool{
+		Name:       p.Name,
 		AutoAssign: true,
 	}
 
diff --git a/internal/k8s/controllers/config_controller.go b/internal/k8s/controllers/config_controller.go
index a785b1c4..d783f93e 100644
--- a/internal/k8s/controllers/config_controller.go
+++ b/internal/k8s/controllers/config_controller.go
@@ -104,6 +104,12 @@ func (r *ConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 		return ctrl.Result{}, err
 	}
 
+	var namespaces corev1.NamespaceList
+	if err := r.List(ctx, &namespaces); err != nil {
+		level.Error(r.Logger).Log("controller", "ConfigReconciler", "message", "failed to get namespaces", "error", err)
+		return ctrl.Result{}, err
+	}
+
 	resources := config.ClusterResources{
 		Pools:              ipAddressPools.Items,
 		Peers:              bgpPeers.Items,
@@ -114,6 +120,7 @@ func (r *ConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 		Communities:        communities.Items,
 		PasswordSecrets:    secrets,
 		Nodes:              nodes.Items,
+		Namespaces:         namespaces.Items,
 	}
 
 	level.Debug(r.Logger).Log("controller", "ConfigReconciler", "metallb CRs and Secrets", dumpClusterResources(&resources))
@@ -153,19 +160,7 @@ func (r *ConfigReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
 func (r *ConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
 	p := predicate.Funcs{
 		UpdateFunc: func(e event.UpdateEvent) bool {
-			newNodeObj, ok := e.ObjectNew.(*corev1.Node)
-			if !ok {
-				return true
-			}
-			oldNodeObj, ok := e.ObjectOld.(*corev1.Node)
-			if !ok {
-				return true
-			}
-			// If there is no changes in node labels, ignore event.
-			if labels.Equals(labels.Set(oldNodeObj.Labels), labels.Set(newNodeObj.Labels)) {
-				return false
-			}
-			return true
+			return filterNodeEvent(e) && filterNamespaceEvent(e)
 		},
 	}
 	return ctrl.NewControllerManagedBy(mgr).
@@ -178,10 +173,42 @@ func (r *ConfigReconciler) SetupWithManager(mgr ctrl.Manager) error {
 		Watches(&source.Kind{Type: &metallbv1beta1.AddressPool{}}, &handler.EnqueueRequestForObject{}).
 		Watches(&source.Kind{Type: &metallbv1beta1.Community{}}, &handler.EnqueueRequestForObject{}).
 		Watches(&source.Kind{Type: &corev1.Secret{}}, &handler.EnqueueRequestForObject{}).
+		Watches(&source.Kind{Type: &corev1.Namespace{}}, &handler.EnqueueRequestForObject{}).
 		WithEventFilter(p).
 		Complete(r)
 }
 
+func filterNodeEvent(e event.UpdateEvent) bool {
+	newNodeObj, ok := e.ObjectNew.(*corev1.Node)
+	if !ok {
+		return true
+	}
+	oldNodeObj, ok := e.ObjectOld.(*corev1.Node)
+	if !ok {
+		return true
+	}
+	if labels.Equals(labels.Set(oldNodeObj.Labels), labels.Set(newNodeObj.Labels)) {
+		return false
+	}
+	return true
+}
+
+func filterNamespaceEvent(e event.UpdateEvent) bool {
+	newNamespaceObj, ok := e.ObjectNew.(*corev1.Namespace)
+	if !ok {
+		return true
+	}
+	oldNamespaceObj, ok := e.ObjectOld.(*corev1.Namespace)
+	if !ok {
+		return true
+	}
+	// If there is no changes in namespace labels, ignore event.
+	if labels.Equals(labels.Set(oldNamespaceObj.Labels), labels.Set(newNamespaceObj.Labels)) {
+		return false
+	}
+	return true
+}
+
 func (r *ConfigReconciler) getSecrets(ctx context.Context) (map[string]corev1.Secret, error) {
 	var secrets corev1.SecretList
 	if err := r.List(ctx, &secrets, client.InNamespace(r.Namespace)); err != nil {
diff --git a/internal/k8s/controllers/pool_controller.go b/internal/k8s/controllers/pool_controller.go
index 60aba27c..3fe000d1 100644
--- a/internal/k8s/controllers/pool_controller.go
+++ b/internal/k8s/controllers/pool_controller.go
@@ -23,10 +23,13 @@ import (
 	"github.com/go-kit/log/level"
 	metallbv1beta1 "go.universe.tf/metallb/api/v1beta1"
 	"go.universe.tf/metallb/internal/config"
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/event"
 	"sigs.k8s.io/controller-runtime/pkg/handler"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
 	"sigs.k8s.io/controller-runtime/pkg/source"
 )
 
@@ -35,7 +38,7 @@ type PoolReconciler struct {
 	Logger         log.Logger
 	Scheme         *runtime.Scheme
 	Namespace      string
-	Handler        func(log.Logger, map[string]*config.Pool) SyncState
+	Handler        func(log.Logger, *config.Pools) SyncState
 	ValidateConfig config.Validate
 	ForceReload    func()
 }
@@ -63,10 +66,17 @@ func (r *PoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
 		return ctrl.Result{}, err
 	}
 
+	var namespaces corev1.NamespaceList
+	if err := r.List(ctx, &namespaces); err != nil {
+		level.Error(r.Logger).Log("controller", "ConfigReconciler", "message", "failed to get namespaces", "error", err)
+		return ctrl.Result{}, err
+	}
+
 	resources := config.ClusterResources{
 		Pools:              ipAddressPools.Items,
 		LegacyAddressPools: addressPools.Items,
 		Communities:        communities.Items,
+		Namespaces:         namespaces.Items,
 	}
 
 	level.Debug(r.Logger).Log("controller", "PoolReconciler", "metallb CRs", dumpClusterResources(&resources))
@@ -104,9 +114,16 @@ func (r *PoolReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.
 }
 
 func (r *PoolReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	p := predicate.Funcs{
+		UpdateFunc: func(e event.UpdateEvent) bool {
+			return filterNodeEvent(e) && filterNamespaceEvent(e)
+		},
+	}
 	return ctrl.NewControllerManagedBy(mgr).
 		For(&metallbv1beta1.IPAddressPool{}).
 		Watches(&source.Kind{Type: &metallbv1beta1.AddressPool{}}, &handler.EnqueueRequestForObject{}).
 		Watches(&source.Kind{Type: &metallbv1beta1.Community{}}, &handler.EnqueueRequestForObject{}).
+		Watches(&source.Kind{Type: &corev1.Namespace{}}, &handler.EnqueueRequestForObject{}).
+		WithEventFilter(p).
 		Complete(r)
 }
diff --git a/internal/k8s/listener.go b/internal/k8s/listener.go
index ecad0720..02b5e255 100644
--- a/internal/k8s/listener.go
+++ b/internal/k8s/listener.go
@@ -16,7 +16,7 @@ type Listener struct {
 	sync.Mutex
 	ServiceChanged func(log.Logger, string, *v1.Service, epslices.EpsOrSlices) controllers.SyncState
 	ConfigChanged  func(log.Logger, *config.Config) controllers.SyncState
-	PoolChanged    func(log.Logger, map[string]*config.Pool) controllers.SyncState
+	PoolChanged    func(log.Logger, *config.Pools) controllers.SyncState
 	NodeChanged    func(log.Logger, *v1.Node) controllers.SyncState
 }
 
@@ -38,7 +38,7 @@ func (l *Listener) NodeHandler(logger log.Logger, node *v1.Node) controllers.Syn
 	return l.NodeChanged(logger, node)
 }
 
-func (l *Listener) PoolHandler(logger log.Logger, pools map[string]*config.Pool) controllers.SyncState {
+func (l *Listener) PoolHandler(logger log.Logger, pools *config.Pools) controllers.SyncState {
 	l.Lock()
 	defer l.Unlock()
 	return l.PoolChanged(logger, pools)
diff --git a/speaker/main.go b/speaker/main.go
index c722b4e1..e372fc71 100644
--- a/speaker/main.go
+++ b/speaker/main.go
@@ -295,11 +295,11 @@ func (c *controller) SetBalancer(l log.Logger, name string, svc *v1.Service, eps
 	}
 
 	l = log.With(l, "pool", poolName)
-	pool := c.config.Pools[poolName]
-	if pool == nil {
+	if c.config.Pools == nil || c.config.Pools.ByName[poolName] == nil {
 		level.Error(l).Log("bug", "true", "msg", "internal error: allocated IP has no matching address pool")
 		return c.deleteBalancer(l, name, "internalError")
 	}
+	pool := c.config.Pools.ByName[poolName]
 
 	if svcIPs, ok := c.svcIPs[name]; ok && !compareIPs(lbIPs, svcIPs) {
 		if st := c.deleteBalancer(l, name, "loadBalancerIPChanged"); st == controllers.SyncStateError {
@@ -402,8 +402,11 @@ func (c *controller) deleteBalancerProtocol(l log.Logger, protocol config.Proto,
 	return controllers.SyncStateSuccess
 }
 
-func poolFor(pools map[string]*config.Pool, ips []net.IP) string {
-	for pname, p := range pools {
+func poolFor(pools *config.Pools, ips []net.IP) string {
+	if pools == nil {
+		return ""
+	}
+	for pname, p := range pools.ByName {
 		cnt := 0
 		for _, ip := range ips {
 			for _, cidr := range p.CIDR {


Test patch:
diff --git a/controller/controller_test.go b/controller/controller_test.go
index 40754151..c558d700 100644
--- a/controller/controller_test.go
+++ b/controller/controller_test.go
@@ -17,6 +17,8 @@ import (
 	"github.com/google/go-cmp/cmp"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/util/sets"
 )
 
 func diffService(a, b *v1.Service) string {
@@ -59,6 +61,14 @@ func statusAssigned(ips []string) v1.ServiceStatus {
 	}
 }
 
+func selector(s string) labels.Selector {
+	ret, err := labels.Parse(s)
+	if err != nil {
+		panic(err)
+	}
+	return ret
+}
+
 // testK8S implements service by recording what the controller wants
 // to do to k8s.
 type testK8S struct {
@@ -112,27 +122,68 @@ func TestControllerMutation(t *testing.T) {
 		ips:    allocator.New(),
 		client: k,
 	}
-	pools := map[string]*config.Pool{
+	pools := &config.Pools{ByName: map[string]*config.Pool{
 		"pool1": {
+			Name:       "pool1",
 			AutoAssign: true,
 			CIDR:       []*net.IPNet{ipnet("1.2.3.0/31")},
 		},
 		"pool2": {
+			Name:       "pool2",
 			AutoAssign: false,
 			CIDR:       []*net.IPNet{ipnet("3.4.5.6/32")},
 		},
 		"pool3": {
+			Name:       "pool3",
 			AutoAssign: true,
 			CIDR:       []*net.IPNet{ipnet("1000::/127")},
 		},
 		"pool4": {
+			Name:       "pool4",
 			AutoAssign: false,
 			CIDR:       []*net.IPNet{ipnet("2000::1/128")},
 		},
 		"pool5": {
+			Name:       "pool5",
 			AutoAssign: true,
 			CIDR:       []*net.IPNet{ipnet("1.2.3.0/31"), ipnet("1000::/127")},
 		},
+		"pool6": {
+			Name:       "pool6",
+			AutoAssign: true,
+			CIDR:       []*net.IPNet{ipnet("7.8.9.0/31")},
+			ServiceAllocations: &config.ServiceAllocation{Namespaces: sets.NewString("test-ns1"),
+				Priority: 10},
+		},
+		"pool7": {
+			Name:       "pool7",
+			AutoAssign: true,
+			CIDR:       []*net.IPNet{ipnet("10.11.12.0/31")},
+			ServiceAllocations: &config.ServiceAllocation{Namespaces: sets.NewString("test-ns1"),
+				Priority: 11},
+		},
+		"pool8": {
+			Name:       "pool8",
+			AutoAssign: true,
+			CIDR:       []*net.IPNet{ipnet("13.14.15.0/31")},
+			ServiceAllocations: &config.ServiceAllocation{ServiceSelectors: []labels.Selector{selector("team=metallb")},
+				Priority: 9},
+		},
+		"pool9": {
+			Name:               "pool9",
+			AutoAssign:         true,
+			CIDR:               []*net.IPNet{ipnet("16.17.18.0/31")},
+			ServiceAllocations: &config.ServiceAllocation{Namespaces: sets.NewString("test-ns2")},
+		},
+		"pool10": {
+			Name:       "pool10",
+			AutoAssign: true,
+			CIDR:       []*net.IPNet{ipnet("19.20.21.0/31")},
+			ServiceAllocations: &config.ServiceAllocation{ServiceSelectors: []labels.Selector{selector("team=metallb")},
+				Priority: 8},
+		},
+	}, ByNamespace: map[string][]string{"test-ns1": {"pool6", "pool7"}, "test-ns2": {"pool9"}},
+		ByServiceSelector: []string{"pool8", "pool10"},
 	}
 
 	l := log.NewNopLogger()
@@ -728,6 +779,72 @@ func TestControllerMutation(t *testing.T) {
 			},
 			wantErr: true,
 		},
+		{
+			desc: "request IP for service from namespace specific ip pool",
+			in: &v1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "test-ns1",
+				},
+				Spec: v1.ServiceSpec{
+					Type:       "LoadBalancer",
+					ClusterIPs: []string{"1.2.3.4"},
+				},
+			},
+			want: &v1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "test-ns1",
+				},
+				Spec: v1.ServiceSpec{
+					Type:       "LoadBalancer",
+					ClusterIPs: []string{"1.2.3.4"},
+				},
+				Status: statusAssigned([]string{"7.8.9.0"}),
+			},
+		},
+		{
+			desc: "request IP for service from no priority namespace specific ip pool",
+			in: &v1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "test-ns2",
+				},
+				Spec: v1.ServiceSpec{
+					Type:       "LoadBalancer",
+					ClusterIPs: []string{"1.2.3.4"},
+				},
+			},
+			want: &v1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "test-ns2",
+				},
+				Spec: v1.ServiceSpec{
+					Type:       "LoadBalancer",
+					ClusterIPs: []string{"1.2.3.4"},
+				},
+				Status: statusAssigned([]string{"16.17.18.0"}),
+			},
+		},
+		{
+			desc: "request IP for service from service label specific ip pool",
+			in: &v1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Labels: map[string]string{"team": "metallb"},
+				},
+				Spec: v1.ServiceSpec{
+					Type:       "LoadBalancer",
+					ClusterIPs: []string{"1.2.3.4"},
+				},
+			},
+			want: &v1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Labels: map[string]string{"team": "metallb"},
+				},
+				Spec: v1.ServiceSpec{
+					Type:       "LoadBalancer",
+					ClusterIPs: []string{"1.2.3.4"},
+				},
+				Status: statusAssigned([]string{"19.20.21.0"}),
+			},
+		},
 	}
 
 	for i := 0; i < 100; i++ {
@@ -810,7 +927,7 @@ func TestControllerConfig(t *testing.T) {
 	// Set an empty config. Balancer should still not do anything to
 	// our unallocated service, and return an error to force a
 	// retry after sync is complete.
-	if c.SetPools(l, map[string]*config.Pool{}) == controllers.SyncStateError {
+	if c.SetPools(l, &config.Pools{ByName: map[string]*config.Pool{}}) == controllers.SyncStateError {
 		t.Fatalf("SetPools with empty config failed")
 	}
 
@@ -823,12 +940,13 @@ func TestControllerConfig(t *testing.T) {
 	}
 
 	// Set a config with some IPs. Still no allocation, not synced.
-	pools := map[string]*config.Pool{
+	pools := &config.Pools{ByName: map[string]*config.Pool{
 		"default": {
+			Name:       "default",
 			AutoAssign: true,
 			CIDR:       []*net.IPNet{ipnet("1.2.3.0/24")},
 		},
-	}
+	}}
 	if c.SetPools(l, pools) == controllers.SyncStateError {
 		t.Fatalf("SetPools failed")
 	}
@@ -854,7 +972,7 @@ func TestControllerConfig(t *testing.T) {
 	}
 
 	// Now that an IP is allocated, removing the IP pool is not allowed.
-	if c.SetPools(l, map[string]*config.Pool{}) != controllers.SyncStateError {
+	if c.SetPools(l, &config.Pools{ByName: map[string]*config.Pool{}}) != controllers.SyncStateError {
 		t.Fatalf("SetPools that deletes allocated IPs was accepted")
 	}
 
@@ -872,12 +990,13 @@ func TestDeleteRecyclesIP(t *testing.T) {
 	}
 
 	l := log.NewNopLogger()
-	pools := map[string]*config.Pool{
+	pools := &config.Pools{ByName: map[string]*config.Pool{
 		"default": {
+			Name:       "default",
 			AutoAssign: true,
 			CIDR:       []*net.IPNet{ipnet("1.2.3.0/32")},
 		},
-	}
+	}}
 	if c.SetPools(l, pools) == controllers.SyncStateError {
 		t.Fatal("SetPools failed")
 	}
@@ -962,7 +1081,7 @@ func TestControllerDualStackConfig(t *testing.T) {
 	// Set an empty config. Balancer should still not do anything to
 	// our unallocated service, and return an error to force a
 	// retry after sync is complete.
-	if c.SetPools(l, map[string]*config.Pool{}) == controllers.SyncStateError {
+	if c.SetPools(l, &config.Pools{ByName: map[string]*config.Pool{}}) == controllers.SyncStateError {
 		t.Fatalf("SetPools with empty config failed")
 	}
 
@@ -975,12 +1094,13 @@ func TestControllerDualStackConfig(t *testing.T) {
 	}
 
 	// Set a config with some IPs. Still no allocation, not synced.
-	pools := map[string]*config.Pool{
+	pools := &config.Pools{ByName: map[string]*config.Pool{
 		"default": {
+			Name:       "default",
 			AutoAssign: true,
 			CIDR:       []*net.IPNet{ipnet("1.2.3.0/24"), ipnet("1000::1/127")},
 		},
-	}
+	}}
 
 	if c.SetPools(l, pools) == controllers.SyncStateError {
 		t.Fatalf("SetPools failed")
@@ -1007,7 +1127,7 @@ func TestControllerDualStackConfig(t *testing.T) {
 	}
 
 	// Now that an IP is allocated, removing the IP pool is not allowed.
-	if c.SetPools(l, map[string]*config.Pool{}) != controllers.SyncStateError {
+	if c.SetPools(l, &config.Pools{ByName: map[string]*config.Pool{}}) != controllers.SyncStateError {
 		t.Fatalf("SetPools that deletes allocated IPs was accepted")
 	}
 
diff --git a/e2etest/l2tests/assignment.go b/e2etest/l2tests/assignment.go
index 08069ad1..a473a440 100644
--- a/e2etest/l2tests/assignment.go
+++ b/e2etest/l2tests/assignment.go
@@ -16,6 +16,8 @@ import (
 	"go.universe.tf/metallb/e2etest/pkg/service"
 	internalconfig "go.universe.tf/metallb/internal/config"
 
+	testservice "go.universe.tf/metallb/e2etest/pkg/service"
+	corev1 "k8s.io/api/core/v1"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	clientset "k8s.io/client-go/kubernetes"
@@ -81,6 +83,10 @@ var _ = ginkgo.Describe("IP Assignment", func() {
 				svc.Name = "singleip1"
 			})
 			framework.ExpectNoError(err)
+			defer func() {
+				testservice.Delete(cs, svc1)
+			}()
+
 			gomega.Consistently(func() int {
 				s, err := cs.CoreV1().Services(svc1.Namespace).Get(context.Background(), svc1.Name, metav1.GetOptions{})
 				framework.ExpectNoError(err)
@@ -116,4 +122,267 @@ var _ = ginkgo.Describe("IP Assignment", func() {
 					return err
 				}))
 	})
+
+	ginkgo.Context("IPV4 - Validate service allocation in address pools", func() {
+		ginkgo.AfterEach(func() {
+			// Clean previous configuration.
+			err := ConfigUpdater.Clean()
+			framework.ExpectNoError(err)
+		})
+
+		ginkgo.It("with namespace", func() {
+			namespacePoolWithLowerPriority := metallbv1beta1.IPAddressPool{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-ns-pool-1"},
+				Spec: metallbv1beta1.IPAddressPoolSpec{
+					Addresses: []string{
+						"192.168.5.0/32",
+					},
+					AllocateTo: &metallbv1beta1.ServiceAllocation{Priority: 20, Namespaces: []string{f.Namespace.Name}},
+				},
+			}
+			namespacePoolWithHigherPriority := metallbv1beta1.IPAddressPool{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-ns-pool-2"},
+				Spec: metallbv1beta1.IPAddressPoolSpec{
+					Addresses: []string{
+						"192.168.10.0/32",
+					},
+					AllocateTo: &metallbv1beta1.ServiceAllocation{Priority: 10, Namespaces: []string{f.Namespace.Name}},
+				},
+			}
+			defaultPool := metallbv1beta1.IPAddressPool{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-ns-pool-3"},
+				Spec: metallbv1beta1.IPAddressPoolSpec{
+					Addresses: []string{
+						"192.168.20.0/32",
+					},
+				},
+			}
+			resources := internalconfig.ClusterResources{
+				Pools: []metallbv1beta1.IPAddressPool{namespacePoolWithLowerPriority, namespacePoolWithHigherPriority, defaultPool},
+			}
+
+			err := ConfigUpdater.Update(resources)
+			framework.ExpectNoError(err)
+
+			svc1, _ := testservice.CreateWithBackend(cs, f.Namespace.Name, "svc-test-ns-pool-1")
+			svc2, _ := testservice.CreateWithBackend(cs, f.Namespace.Name, "svc-test-ns-pool-2")
+			svc3, _ := testservice.CreateWithBackend(cs, f.Namespace.Name, "svc-test-ns-pool-3")
+			defer func() {
+				testservice.Delete(cs, svc1)
+				testservice.Delete(cs, svc2)
+				testservice.Delete(cs, svc3)
+			}()
+
+			// The createWithBackend method always wait for service to acquire an ingress IP, so
+			// just validate service ingress ip address are assigned from appropriate ip
+			// address pool.
+			ginkgo.By("validate LoadBalancer IP is allocated from 1st higher priority address pool")
+			err = config.ValidateIPInRange([]metallbv1beta1.IPAddressPool{namespacePoolWithHigherPriority}, e2eservice.GetIngressPoint(
+				&svc1.Status.LoadBalancer.Ingress[0]))
+			framework.ExpectNoError(err)
+			ginkgo.By("validate LoadBalancer IP is allocated from 2nd higher priority address pool")
+			err = config.ValidateIPInRange([]metallbv1beta1.IPAddressPool{namespacePoolWithLowerPriority}, e2eservice.GetIngressPoint(
+				&svc2.Status.LoadBalancer.Ingress[0]))
+			framework.ExpectNoError(err)
+			ginkgo.By("validate LoadBalancer IP is allocated from default address pool")
+			err = config.ValidateIPInRange([]metallbv1beta1.IPAddressPool{defaultPool}, e2eservice.GetIngressPoint(
+				&svc3.Status.LoadBalancer.Ingress[0]))
+			framework.ExpectNoError(err)
+		})
+
+		ginkgo.It("with namespace and namespace labels", func() {
+			namespacePoolWithLowerPriority := metallbv1beta1.IPAddressPool{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-ns-label-pool-1"},
+				Spec: metallbv1beta1.IPAddressPoolSpec{
+					Addresses: []string{
+						"192.168.5.0/32",
+					},
+					AllocateTo: &metallbv1beta1.ServiceAllocation{Priority: 20, Namespaces: []string{f.Namespace.Name}},
+				},
+			}
+			namespaceLabelPoolWithHigherPriority := metallbv1beta1.IPAddressPool{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-ns-label-pool-2"},
+				Spec: metallbv1beta1.IPAddressPoolSpec{
+					Addresses: []string{
+						"192.168.10.0/32",
+					},
+					AllocateTo: &metallbv1beta1.ServiceAllocation{Priority: 10, NamespaceSelectors: []metav1.LabelSelector{{MatchLabels: f.Namespace.Labels}}},
+				},
+			}
+			defaultPool := metallbv1beta1.IPAddressPool{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-ns-label-pool-3"},
+				Spec: metallbv1beta1.IPAddressPoolSpec{
+					Addresses: []string{
+						"192.168.20.0/32",
+					},
+				},
+			}
+
+			resources := internalconfig.ClusterResources{
+				Pools: []metallbv1beta1.IPAddressPool{namespacePoolWithLowerPriority, namespaceLabelPoolWithHigherPriority, defaultPool},
+			}
+			err := ConfigUpdater.Update(resources)
+			framework.ExpectNoError(err)
+
+			svc1, _ := testservice.CreateWithBackend(cs, f.Namespace.Name, "svc-test-ns-label-pool-1")
+			svc2, _ := testservice.CreateWithBackend(cs, f.Namespace.Name, "svc-test-ns-label-pool-2")
+			svc3, _ := testservice.CreateWithBackend(cs, f.Namespace.Name, "svc-test-ns-label-pool-3")
+			defer func() {
+				testservice.Delete(cs, svc1)
+				testservice.Delete(cs, svc2)
+				testservice.Delete(cs, svc3)
+			}()
+
+			// The createWithBackend method always wait for service to acquire an ingress IP, so
+			// just validate service ingress ip address are assigned from appropriate ip
+			// address pool.
+			ginkgo.By("validate LoadBalancer IP is allocated from 1st higher priority address pool")
+			err = config.ValidateIPInRange([]metallbv1beta1.IPAddressPool{namespaceLabelPoolWithHigherPriority}, e2eservice.GetIngressPoint(
+				&svc1.Status.LoadBalancer.Ingress[0]))
+			framework.ExpectNoError(err)
+			ginkgo.By("validate LoadBalancer IP is allocated from 2nd higher priority address pool")
+			err = config.ValidateIPInRange([]metallbv1beta1.IPAddressPool{namespacePoolWithLowerPriority}, e2eservice.GetIngressPoint(
+				&svc2.Status.LoadBalancer.Ingress[0]))
+			framework.ExpectNoError(err)
+			ginkgo.By("validate LoadBalancer IP is allocated from default address pool")
+			err = config.ValidateIPInRange([]metallbv1beta1.IPAddressPool{defaultPool}, e2eservice.GetIngressPoint(
+				&svc3.Status.LoadBalancer.Ingress[0]))
+			framework.ExpectNoError(err)
+		})
+
+		ginkgo.It("with service label", func() {
+			svcLabelPoolWithLowerPriority := metallbv1beta1.IPAddressPool{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-svc-label-pool-1"},
+				Spec: metallbv1beta1.IPAddressPoolSpec{
+					Addresses: []string{
+						"192.168.5.0/32",
+					},
+					AllocateTo: &metallbv1beta1.ServiceAllocation{Priority: 20,
+						ServiceSelectors: []metav1.LabelSelector{{MatchLabels: map[string]string{"test": "e2e"}}}},
+				},
+			}
+			svcLabelPoolWithHigherPriority := metallbv1beta1.IPAddressPool{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-svc-label-pool-2"},
+				Spec: metallbv1beta1.IPAddressPoolSpec{
+					Addresses: []string{
+						"192.168.10.0/32",
+					},
+					AllocateTo: &metallbv1beta1.ServiceAllocation{Priority: 10,
+						ServiceSelectors: []metav1.LabelSelector{{MatchLabels: map[string]string{"test": "e2e"}}}},
+				},
+			}
+			defaultPool := metallbv1beta1.IPAddressPool{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-svc-label-pool-3"},
+				Spec: metallbv1beta1.IPAddressPoolSpec{
+					Addresses: []string{
+						"192.168.20.0/32",
+					},
+				},
+			}
+
+			resources := internalconfig.ClusterResources{
+				Pools: []metallbv1beta1.IPAddressPool{svcLabelPoolWithLowerPriority, svcLabelPoolWithHigherPriority, defaultPool},
+			}
+			err := ConfigUpdater.Update(resources)
+			framework.ExpectNoError(err)
+
+			svcTweakWithLabel := func(svc *corev1.Service) {
+				if svc.Labels == nil {
+					svc.Labels = make(map[string]string)
+				}
+				svc.Labels["test"] = "e2e"
+			}
+			svc1, _ := testservice.CreateWithBackend(cs, f.Namespace.Name, "svc-test-svc-label-pool-1", svcTweakWithLabel)
+			svc2, _ := testservice.CreateWithBackend(cs, f.Namespace.Name, "svc-test-svc-label-pool-2", svcTweakWithLabel)
+			svc3, _ := testservice.CreateWithBackend(cs, f.Namespace.Name, "svc-test-svc-label-pool-3", svcTweakWithLabel)
+			defer func() {
+				testservice.Delete(cs, svc1)
+				testservice.Delete(cs, svc2)
+				testservice.Delete(cs, svc3)
+			}()
+
+			// The createWithBackend method always wait for service to acquire an ingress IP, so
+			// just validate service ingress ip address are assigned from appropriate ip
+			// address pool.
+			ginkgo.By("validate LoadBalancer IP is allocated from 1st higher priority address pool")
+			err = config.ValidateIPInRange([]metallbv1beta1.IPAddressPool{svcLabelPoolWithHigherPriority}, e2eservice.GetIngressPoint(
+				&svc1.Status.LoadBalancer.Ingress[0]))
+			framework.ExpectNoError(err)
+			ginkgo.By("validate LoadBalancer IP is allocated from 2nd higher priority address pool")
+			err = config.ValidateIPInRange([]metallbv1beta1.IPAddressPool{svcLabelPoolWithLowerPriority}, e2eservice.GetIngressPoint(
+				&svc2.Status.LoadBalancer.Ingress[0]))
+			framework.ExpectNoError(err)
+			ginkgo.By("validate LoadBalancer IP is allocated from default address pool")
+			err = config.ValidateIPInRange([]metallbv1beta1.IPAddressPool{defaultPool}, e2eservice.GetIngressPoint(
+				&svc3.Status.LoadBalancer.Ingress[0]))
+			framework.ExpectNoError(err)
+		})
+
+		ginkgo.It("with namespace and service label", func() {
+			namespacePoolWithLowerPriority := metallbv1beta1.IPAddressPool{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-ns-svc-label-pool-1"},
+				Spec: metallbv1beta1.IPAddressPoolSpec{
+					Addresses: []string{
+						"192.168.5.0/32",
+					},
+					AllocateTo: &metallbv1beta1.ServiceAllocation{Priority: 20, Namespaces: []string{f.Namespace.Name}},
+				},
+			}
+			svcLabelPoolWithHigherPriority := metallbv1beta1.IPAddressPool{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-ns-svc-label-pool-2"},
+				Spec: metallbv1beta1.IPAddressPoolSpec{
+					Addresses: []string{
+						"192.168.10.0/32",
+					},
+					AllocateTo: &metallbv1beta1.ServiceAllocation{Priority: 10,
+						ServiceSelectors: []metav1.LabelSelector{{MatchLabels: map[string]string{"test": "e2e"}}}},
+				},
+			}
+			defaultPool := metallbv1beta1.IPAddressPool{
+				ObjectMeta: metav1.ObjectMeta{Name: "test-ns-svc-label-pool-3"},
+				Spec: metallbv1beta1.IPAddressPoolSpec{
+					Addresses: []string{
+						"192.168.20.0/32",
+					},
+				},
+			}
+
+			resources := internalconfig.ClusterResources{
+				Pools: []metallbv1beta1.IPAddressPool{namespacePoolWithLowerPriority, svcLabelPoolWithHigherPriority, defaultPool},
+			}
+			err := ConfigUpdater.Update(resources)
+			framework.ExpectNoError(err)
+
+			svcTweakWithLabel := func(svc *corev1.Service) {
+				if svc.Labels == nil {
+					svc.Labels = make(map[string]string)
+				}
+				svc.Labels["test"] = "e2e"
+			}
+			svc1, _ := testservice.CreateWithBackend(cs, f.Namespace.Name, "svc-ns-svc-label-pool-1", svcTweakWithLabel)
+			svc2, _ := testservice.CreateWithBackend(cs, f.Namespace.Name, "svc-ns-svc-label-pool-2", svcTweakWithLabel)
+			svc3, _ := testservice.CreateWithBackend(cs, f.Namespace.Name, "svc-ns-svc-label-pool-3", svcTweakWithLabel)
+			defer func() {
+				testservice.Delete(cs, svc1)
+				testservice.Delete(cs, svc2)
+				testservice.Delete(cs, svc3)
+			}()
+
+			// The createWithBackend method always wait for service to acquire an ingress IP, so
+			// just validate service ingress ip address are assigned from appropriate ip
+			// address pool.
+			ginkgo.By("validate LoadBalancer IP is allocated from 1st higher priority address pool")
+			err = config.ValidateIPInRange([]metallbv1beta1.IPAddressPool{svcLabelPoolWithHigherPriority}, e2eservice.GetIngressPoint(
+				&svc1.Status.LoadBalancer.Ingress[0]))
+			framework.ExpectNoError(err)
+			ginkgo.By("validate LoadBalancer IP is allocated from 2nd higher priority address pool")
+			err = config.ValidateIPInRange([]metallbv1beta1.IPAddressPool{namespacePoolWithLowerPriority}, e2eservice.GetIngressPoint(
+				&svc2.Status.LoadBalancer.Ingress[0]))
+			framework.ExpectNoError(err)
+			ginkgo.By("validate LoadBalancer IP is allocated from default address pool")
+			err = config.ValidateIPInRange([]metallbv1beta1.IPAddressPool{defaultPool}, e2eservice.GetIngressPoint(
+				&svc3.Status.LoadBalancer.Ingress[0]))
+			framework.ExpectNoError(err)
+		})
+	})
 })
diff --git a/internal/allocator/allocator_test.go b/internal/allocator/allocator_test.go
index bf2abab7..df4673d6 100644
--- a/internal/allocator/allocator_test.go
+++ b/internal/allocator/allocator_test.go
@@ -18,8 +18,9 @@ import (
 
 func TestAssignment(t *testing.T) {
 	alloc := New()
-	if err := alloc.SetPools(map[string]*config.Pool{
+	if err := alloc.SetPools(&config.Pools{ByName: map[string]*config.Pool{
 		"test": {
+			Name:       "test",
 			AutoAssign: true,
 			CIDR: []*net.IPNet{
 				ipnet("1.2.3.4/31"),
@@ -27,6 +28,7 @@ func TestAssignment(t *testing.T) {
 			},
 		},
 		"test2": {
+			Name:          "test2",
 			AvoidBuggyIPs: true,
 			AutoAssign:    true,
 			CIDR: []*net.IPNet{
@@ -34,7 +36,7 @@ func TestAssignment(t *testing.T) {
 				ipnet("1000::4:0/120"),
 			},
 		},
-	}); err != nil {
+	}}); err != nil {
 		t.Fatalf("SetPools: %s", err)
 	}
 
@@ -377,12 +379,14 @@ func TestPoolAllocation(t *testing.T) {
 	// This test only allocates from the "test" pool, so it will run
 	// out of IPs quickly even though there are tons available in
 	// other pools.
-	if err := alloc.SetPools(map[string]*config.Pool{
+	if err := alloc.SetPools(&config.Pools{ByName: map[string]*config.Pool{
 		"not_this_one": {
+			Name:       "not_this_one",
 			AutoAssign: true,
 			CIDR:       []*net.IPNet{ipnet("192.168.0.0/16"), ipnet("fc00::1:0/112")},
 		},
 		"test": {
+			Name:       "test",
 			AutoAssign: true,
 			CIDR: []*net.IPNet{
 				ipnet("1.2.3.4/31"),
@@ -392,10 +396,11 @@ func TestPoolAllocation(t *testing.T) {
 			},
 		},
 		"test2": {
+			Name:       "test2",
 			AutoAssign: true,
 			CIDR:       []*net.IPNet{ipnet("10.20.30.0/24"), ipnet("fc00::2:0/120")},
 		},
-	}); err != nil {
+	}}); err != nil {
 		t.Fatalf("SetPools: %s", err)
 	}
 
@@ -775,16 +780,18 @@ func TestPoolAllocation(t *testing.T) {
 
 func TestAllocation(t *testing.T) {
 	alloc := New()
-	if err := alloc.SetPools(map[string]*config.Pool{
+	if err := alloc.SetPools(&config.Pools{ByName: map[string]*config.Pool{
 		"test1": {
+			Name:       "test1",
 			AutoAssign: true,
 			CIDR:       []*net.IPNet{ipnet("1.2.3.4/31"), ipnet("1000::4/127")},
 		},
 		"test2": {
+			Name:       "test2",
 			AutoAssign: true,
 			CIDR:       []*net.IPNet{ipnet("1.2.3.10/31"), ipnet("1000::10/127")},
 		},
-	}); err != nil {
+	}}); err != nil {
 		t.Fatalf("SetPools: %s", err)
 	}
 
@@ -1076,7 +1083,7 @@ func TestAllocation(t *testing.T) {
 			alloc.Unassign(test.svc)
 			continue
 		}
-		ips, err := alloc.Allocate(test.svc, test.ipFamily, test.ports, test.sharingKey, "")
+		ips, err := alloc.Allocate(test.svc, nil, test.ipFamily, test.ports, test.sharingKey, "")
 		if test.wantErr {
 			if err == nil {
 				t.Errorf("%s: should have caused an error, but did not", test.desc)
@@ -1103,26 +1110,30 @@ func TestAllocation(t *testing.T) {
 
 func TestBuggyIPs(t *testing.T) {
 	alloc := New()
-	if err := alloc.SetPools(map[string]*config.Pool{
+	if err := alloc.SetPools(&config.Pools{ByName: map[string]*config.Pool{
 		"test": {
+			Name:       "test",
 			AutoAssign: true,
 			CIDR:       []*net.IPNet{ipnet("1.2.3.0/31")},
 		},
 		"test2": {
+			Name:       "test2",
 			AutoAssign: true,
 			CIDR:       []*net.IPNet{ipnet("1.2.3.254/31")},
 		},
 		"test3": {
+			Name:          "test3",
 			AvoidBuggyIPs: true,
 			AutoAssign:    true,
 			CIDR:          []*net.IPNet{ipnet("1.2.4.0/31")},
 		},
 		"test4": {
+			Name:          "test4",
 			AvoidBuggyIPs: true,
 			AutoAssign:    true,
 			CIDR:          []*net.IPNet{ipnet("1.2.4.254/31")},
 		},
-	}); err != nil {
+	}}); err != nil {
 		t.Fatalf("SetPools: %s", err)
 	}
 
@@ -1152,7 +1163,7 @@ func TestBuggyIPs(t *testing.T) {
 	}
 
 	for i, test := range tests {
-		ips, err := alloc.Allocate(test.svc, ipfamily.IPv4, nil, "", "")
+		ips, err := alloc.Allocate(test.svc, nil, ipfamily.IPv4, nil, "", "")
 		if test.wantErr {
 			if err == nil {
 				t.Errorf("#%d should have caused an error, but did not", i+1)
@@ -1174,12 +1185,13 @@ func TestBuggyIPs(t *testing.T) {
 
 func TestConfigReload(t *testing.T) {
 	alloc := New()
-	if err := alloc.SetPools(map[string]*config.Pool{
+	if err := alloc.SetPools(&config.Pools{ByName: map[string]*config.Pool{
 		"test": {
+			Name:       "test",
 			AutoAssign: true,
 			CIDR:       []*net.IPNet{ipnet("1.2.3.0/30"), ipnet("1000::/126")},
 		},
-	}); err != nil {
+	}}); err != nil {
 		t.Fatalf("SetPools: %s", err)
 	}
 	if err := alloc.Assign("s1", []net.IP{net.ParseIP("1.2.3.0")}, nil, "", ""); err != nil {
@@ -1198,6 +1210,7 @@ func TestConfigReload(t *testing.T) {
 			desc: "set same config is no-op",
 			pools: map[string]*config.Pool{
 				"test": {
+					Name:       "test",
 					AutoAssign: true,
 					CIDR:       []*net.IPNet{ipnet("1.2.3.0/30"), ipnet("1000::/126")},
 				},
@@ -1208,6 +1221,7 @@ func TestConfigReload(t *testing.T) {
 			desc: "expand pool",
 			pools: map[string]*config.Pool{
 				"test": {
+					Name:       "test",
 					AutoAssign: true,
 					CIDR:       []*net.IPNet{ipnet("1.2.3.0/24"), ipnet("1000::/120")},
 				},
@@ -1218,6 +1232,7 @@ func TestConfigReload(t *testing.T) {
 			desc: "shrink pool",
 			pools: map[string]*config.Pool{
 				"test": {
+					Name:       "test",
 					AutoAssign: true,
 					CIDR:       []*net.IPNet{ipnet("1.2.3.0/30"), ipnet("1000::/126")},
 				},
@@ -1228,6 +1243,7 @@ func TestConfigReload(t *testing.T) {
 			desc: "can't shrink further",
 			pools: map[string]*config.Pool{
 				"test": {
+					Name:       "test",
 					AutoAssign: true,
 					CIDR:       []*net.IPNet{ipnet("1.2.3.2/31"), ipnet("1000::0/126")},
 				},
@@ -1239,6 +1255,7 @@ func TestConfigReload(t *testing.T) {
 			desc: "can't shrink further ipv6",
 			pools: map[string]*config.Pool{
 				"test": {
+					Name:       "test",
 					AutoAssign: true,
 					CIDR:       []*net.IPNet{ipnet("1.2.3.0/30"), ipnet("1000::2/127")},
 				},
@@ -1250,6 +1267,7 @@ func TestConfigReload(t *testing.T) {
 			desc: "rename the pool",
 			pools: map[string]*config.Pool{
 				"test2": {
+					Name:       "test2",
 					AutoAssign: true,
 					CIDR:       []*net.IPNet{ipnet("1.2.3.0/30"), ipnet("1000::0/126")},
 				},
@@ -1260,10 +1278,12 @@ func TestConfigReload(t *testing.T) {
 			desc: "split pool",
 			pools: map[string]*config.Pool{
 				"test": {
+					Name:       "test",
 					AutoAssign: true,
 					CIDR:       []*net.IPNet{ipnet("1.2.3.0/31"), ipnet("1000::/127")},
 				},
 				"test2": {
+					Name:       "test2",
 					AutoAssign: true,
 					CIDR:       []*net.IPNet{ipnet("1.2.3.2/31"), ipnet("1000::2/127")},
 				},
@@ -1274,10 +1294,12 @@ func TestConfigReload(t *testing.T) {
 			desc: "swap pool names",
 			pools: map[string]*config.Pool{
 				"test2": {
+					Name:       "test2",
 					AutoAssign: true,
 					CIDR:       []*net.IPNet{ipnet("1.2.3.0/31"), ipnet("1000::/127")},
 				},
 				"test": {
+					Name:       "test",
 					AutoAssign: true,
 					CIDR:       []*net.IPNet{ipnet("1.2.3.2/31"), ipnet("1000::2/127")},
 				},
@@ -1288,6 +1310,7 @@ func TestConfigReload(t *testing.T) {
 			desc: "delete used pool",
 			pools: map[string]*config.Pool{
 				"test": {
+					Name:       "test",
 					AutoAssign: true,
 					CIDR:       []*net.IPNet{ipnet("1.2.3.2/31"), ipnet("1000::/126")},
 				},
@@ -1299,6 +1322,7 @@ func TestConfigReload(t *testing.T) {
 			desc: "delete used pool ipv6",
 			pools: map[string]*config.Pool{
 				"test": {
+					Name:       "test",
 					AutoAssign: true,
 					CIDR:       []*net.IPNet{ipnet("1.2.3.0/30"), ipnet("1000::2/127")},
 				},
@@ -1310,6 +1334,7 @@ func TestConfigReload(t *testing.T) {
 			desc: "delete unused pool",
 			pools: map[string]*config.Pool{
 				"test2": {
+					Name:       "test2",
 					AutoAssign: true,
 					CIDR:       []*net.IPNet{ipnet("1.2.3.0/31"), ipnet("1000::/127")},
 				},
@@ -1320,6 +1345,7 @@ func TestConfigReload(t *testing.T) {
 			desc: "enable buggy IPs not allowed",
 			pools: map[string]*config.Pool{
 				"test2": {
+					Name:          "test2",
 					AutoAssign:    true,
 					AvoidBuggyIPs: true,
 					CIDR:          []*net.IPNet{ipnet("1.2.3.0/31"), ipnet("1000::/127")},
@@ -1331,7 +1357,7 @@ func TestConfigReload(t *testing.T) {
 	}
 
 	for _, test := range tests {
-		err := alloc.SetPools(test.pools)
+		err := alloc.SetPools(&config.Pools{ByName: test.pools})
 		if test.wantErr {
 			if err == nil {
 				t.Errorf("%q should have failed to SetPools, but succeeded", test.desc)
@@ -1348,16 +1374,18 @@ func TestConfigReload(t *testing.T) {
 
 func TestAutoAssign(t *testing.T) {
 	alloc := New()
-	if err := alloc.SetPools(map[string]*config.Pool{
+	if err := alloc.SetPools(&config.Pools{ByName: map[string]*config.Pool{
 		"test1": {
+			Name:       "test1",
 			AutoAssign: false,
 			CIDR:       []*net.IPNet{ipnet("1.2.3.4/31"), ipnet("1000::4/127")},
 		},
 		"test2": {
+			Name:       "test2",
 			AutoAssign: true,
 			CIDR:       []*net.IPNet{ipnet("1.2.3.10/31"), ipnet("1000::10/127")},
 		},
-	}); err != nil {
+	}}); err != nil {
 		t.Fatalf("SetPools: %s", err)
 	}
 
@@ -1500,7 +1528,7 @@ func TestAutoAssign(t *testing.T) {
 			alloc.Unassign(test.svc)
 			continue
 		}
-		ips, err := alloc.Allocate(test.svc, test.ipFamily, nil, "", "")
+		ips, err := alloc.Allocate(test.svc, nil, test.ipFamily, nil, "", "")
 		if test.wantErr {
 			if err == nil {
 				t.Errorf("#%d should have caused an error, but did not", i+1)
@@ -1572,15 +1600,16 @@ func TestPoolCount(t *testing.T) {
 
 func TestPoolMetrics(t *testing.T) {
 	alloc := New()
-	if err := alloc.SetPools(map[string]*config.Pool{
+	if err := alloc.SetPools(&config.Pools{ByName: map[string]*config.Pool{
 		"test": {
+			Name:       "test",
 			AutoAssign: true,
 			CIDR: []*net.IPNet{
 				ipnet("1.2.3.4/30"),
 				ipnet("1000::4/126"),
 			},
 		},
-	}); err != nil {
+	}}); err != nil {
 		t.Fatalf("SetPools: %s", err)
 	}
 
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index d67c8197..22bbb699 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -15,6 +15,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/util/sets"
 )
 
 func selector(s string) labels.Selector {
@@ -53,7 +54,7 @@ func TestParse(t *testing.T) {
 			desc: "empty config",
 			crs:  ClusterResources{},
 			want: &Config{
-				Pools:       map[string]*Pool{},
+				Pools:       &Pools{ByName: map[string]*Pool{}},
 				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
@@ -242,8 +243,9 @@ func TestParse(t *testing.T) {
 						EBGPMultiHop:  false,
 					},
 				},
-				Pools: map[string]*Pool{
+				Pools: &Pools{ByName: map[string]*Pool{
 					"pool1": {
+						Name:          "pool1",
 						CIDR:          []*net.IPNet{ipnet("10.20.0.0/16"), ipnet("10.50.0.0/24")},
 						AvoidBuggyIPs: true,
 						AutoAssign:    false,
@@ -271,6 +273,7 @@ func TestParse(t *testing.T) {
 						}},
 					},
 					"pool2": {
+						Name:       "pool2",
 						CIDR:       []*net.IPNet{ipnet("30.0.0.0/8")},
 						AutoAssign: true,
 						BGPAdvertisements: []*BGPAdvertisement{
@@ -287,6 +290,7 @@ func TestParse(t *testing.T) {
 						}},
 					},
 					"pool3": {
+						Name: "pool3",
 						CIDR: []*net.IPNet{
 							ipnet("40.0.0.0/25"),
 							ipnet("40.0.0.150/31"),
@@ -308,6 +312,7 @@ func TestParse(t *testing.T) {
 						AutoAssign: true,
 					},
 					"pool4": {
+						Name: "pool4",
 						CIDR: []*net.IPNet{ipnet("2001:db8::/64")},
 						L2Advertisements: []*L2Advertisement{&L2Advertisement{
 							Nodes:         map[string]bool{},
@@ -315,11 +320,215 @@ func TestParse(t *testing.T) {
 						}},
 						AutoAssign: true,
 					},
+				}},
+				BFDProfiles: map[string]*BFDProfile{},
+			},
+		},
+
+		{
+			desc: "ip address pool with namespace selection",
+			crs: ClusterResources{
+				Pools: []v1beta1.IPAddressPool{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "pool1",
+						},
+						Spec: v1beta1.IPAddressPoolSpec{
+							Addresses: []string{
+								"10.20.0.0/16",
+								"10.50.0.0/24",
+							},
+							AvoidBuggyIPs: true,
+							AutoAssign:    pointer.BoolPtr(false),
+							AllocateTo: &v1beta1.ServiceAllocation{Priority: 1,
+								Namespaces: []string{"test-ns1"}},
+						},
+					},
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "pool2",
+						},
+						Spec: v1beta1.IPAddressPoolSpec{
+							Addresses: []string{
+								"30.0.0.0/8",
+							},
+							AllocateTo: &v1beta1.ServiceAllocation{Priority: 2,
+								NamespaceSelectors: []v1.LabelSelector{{MatchLabels: map[string]string{"team": "metallb"}}}},
+						},
+					},
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "pool3",
+						},
+						Spec: v1beta1.IPAddressPoolSpec{
+							Addresses: []string{
+								"40.0.0.0/8",
+							},
+							AllocateTo: &v1beta1.ServiceAllocation{Priority: 3,
+								NamespaceSelectors: []v1.LabelSelector{{MatchLabels: map[string]string{"team": "red"}}}},
+						},
+					},
+				},
+				Namespaces: []corev1.Namespace{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "test-ns1",
+						},
+					},
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name:   "test-ns2",
+							Labels: map[string]string{"team": "metallb"},
+						},
+					},
+				},
+			},
+			want: &Config{
+				Pools: &Pools{ByName: map[string]*Pool{
+					"pool1": {
+						Name:               "pool1",
+						CIDR:               []*net.IPNet{ipnet("10.20.0.0/16"), ipnet("10.50.0.0/24")},
+						AvoidBuggyIPs:      true,
+						AutoAssign:         false,
+						ServiceAllocations: &ServiceAllocation{Priority: 1, Namespaces: sets.String{"test-ns1": {}}},
+					},
+					"pool2": {
+						Name:               "pool2",
+						CIDR:               []*net.IPNet{ipnet("30.0.0.0/8")},
+						AutoAssign:         true,
+						ServiceAllocations: &ServiceAllocation{Priority: 2, Namespaces: sets.String{"test-ns2": {}}},
+					},
+					"pool3": {
+						Name:               "pool3",
+						CIDR:               []*net.IPNet{ipnet("40.0.0.0/8")},
+						AutoAssign:         true,
+						ServiceAllocations: &ServiceAllocation{Priority: 3, Namespaces: sets.String{}},
+					},
+				},
+					ByNamespace: map[string][]string{"test-ns1": {"pool1"}, "test-ns2": {"pool2"}},
 				},
 				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
 
+		{
+			desc: "ip address pool with service selection",
+			crs: ClusterResources{
+				Pools: []v1beta1.IPAddressPool{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "pool1",
+						},
+						Spec: v1beta1.IPAddressPoolSpec{
+							Addresses: []string{
+								"30.0.0.0/8",
+							},
+							AllocateTo: &v1beta1.ServiceAllocation{Priority: 2,
+								ServiceSelectors: []v1.LabelSelector{{MatchLabels: map[string]string{"team": "metallb"}}}},
+						},
+					},
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "pool2",
+						},
+						Spec: v1beta1.IPAddressPoolSpec{
+							Addresses: []string{
+								"40.0.0.0/8",
+							},
+							AllocateTo: &v1beta1.ServiceAllocation{Priority: 3,
+								ServiceSelectors: []v1.LabelSelector{{MatchLabels: map[string]string{"team": "red"}}}},
+						},
+					},
+				},
+			},
+			want: &Config{
+				Pools: &Pools{ByName: map[string]*Pool{
+					"pool1": {
+						Name:               "pool1",
+						CIDR:               []*net.IPNet{ipnet("30.0.0.0/8")},
+						AutoAssign:         true,
+						ServiceAllocations: &ServiceAllocation{Priority: 2, ServiceSelectors: []labels.Selector{selector("team=metallb")}},
+					},
+					"pool2": {
+						Name:               "pool2",
+						CIDR:               []*net.IPNet{ipnet("40.0.0.0/8")},
+						AutoAssign:         true,
+						ServiceAllocations: &ServiceAllocation{Priority: 3, ServiceSelectors: []labels.Selector{selector("team=red")}},
+					},
+				},
+					ByServiceSelector: []string{"pool1", "pool2"}},
+				BFDProfiles: map[string]*BFDProfile{},
+			},
+		},
+
+		{
+			desc: "ip address pool with namespace and service selection",
+			crs: ClusterResources{
+				Pools: []v1beta1.IPAddressPool{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "pool1",
+						},
+						Spec: v1beta1.IPAddressPoolSpec{
+							Addresses: []string{
+								"30.0.0.0/8",
+							},
+							AllocateTo: &v1beta1.ServiceAllocation{Priority: 2,
+								Namespaces:       []string{"test-ns1"},
+								ServiceSelectors: []v1.LabelSelector{{MatchLabels: map[string]string{"testsvc-1": "1"}}}},
+						},
+					},
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "pool2",
+						},
+						Spec: v1beta1.IPAddressPoolSpec{
+							Addresses: []string{
+								"40.0.0.0/8",
+							},
+							AllocateTo: &v1beta1.ServiceAllocation{Priority: 3,
+								NamespaceSelectors: []v1.LabelSelector{{MatchLabels: map[string]string{"team": "metallb"}}},
+								ServiceSelectors:   []v1.LabelSelector{{MatchLabels: map[string]string{"testsvc-2": "2"}}}},
+						},
+					},
+				},
+				Namespaces: []corev1.Namespace{
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name: "test-ns1",
+						},
+					},
+					{
+						ObjectMeta: v1.ObjectMeta{
+							Name:   "test-ns2",
+							Labels: map[string]string{"team": "metallb"},
+						},
+					},
+				},
+			},
+			want: &Config{
+				Pools: &Pools{ByName: map[string]*Pool{
+					"pool1": {
+						Name:       "pool1",
+						CIDR:       []*net.IPNet{ipnet("30.0.0.0/8")},
+						AutoAssign: true,
+						ServiceAllocations: &ServiceAllocation{Priority: 2, Namespaces: sets.NewString("test-ns1"),
+							ServiceSelectors: []labels.Selector{selector("testsvc-1=1")}},
+					},
+					"pool2": {
+						Name:       "pool2",
+						CIDR:       []*net.IPNet{ipnet("40.0.0.0/8")},
+						AutoAssign: true,
+						ServiceAllocations: &ServiceAllocation{Priority: 3, Namespaces: sets.NewString("test-ns2"),
+							ServiceSelectors: []labels.Selector{selector("testsvc-2=2")}},
+					},
+				},
+					ByNamespace:       map[string][]string{"test-ns1": {"pool1"}, "test-ns2": {"pool2"}},
+					ByServiceSelector: []string{"pool1", "pool2"}},
+				BFDProfiles: map[string]*BFDProfile{},
+			},
+		},
+
 		{
 			desc: "peer-only",
 			crs: ClusterResources{
@@ -345,7 +554,7 @@ func TestParse(t *testing.T) {
 						EBGPMultiHop:  false,
 					},
 				},
-				Pools:       map[string]*Pool{},
+				Pools:       &Pools{ByName: map[string]*Pool{}},
 				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
@@ -460,7 +669,7 @@ func TestParse(t *testing.T) {
 						NodeSelectors: []labels.Selector{labels.Everything()},
 					},
 				},
-				Pools:       map[string]*Pool{},
+				Pools:       &Pools{ByName: map[string]*Pool{}},
 				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
@@ -695,8 +904,9 @@ func TestParse(t *testing.T) {
 				},
 			},
 			want: &Config{
-				Pools: map[string]*Pool{
+				Pools: &Pools{ByName: map[string]*Pool{
 					"pool1": {
+						Name:       "pool1",
 						AutoAssign: true,
 						CIDR:       []*net.IPNet{ipnet("1.2.3.0/24")},
 						BGPAdvertisements: []*BGPAdvertisement{
@@ -708,7 +918,7 @@ func TestParse(t *testing.T) {
 							},
 						},
 					},
-				},
+				}},
 				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
@@ -734,8 +944,9 @@ func TestParse(t *testing.T) {
 				},
 			},
 			want: &Config{
-				Pools: map[string]*Pool{
+				Pools: &Pools{ByName: map[string]*Pool{
 					"pool1": {
+						Name:       "pool1",
 						AutoAssign: true,
 						CIDR:       []*net.IPNet{ipnet("1.2.3.0/24")},
 						BGPAdvertisements: []*BGPAdvertisement{
@@ -747,7 +958,7 @@ func TestParse(t *testing.T) {
 							},
 						},
 					},
-				},
+				}},
 				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
@@ -818,8 +1029,9 @@ func TestParse(t *testing.T) {
 				},
 			},
 			want: &Config{
-				Pools: map[string]*Pool{
+				Pools: &Pools{ByName: map[string]*Pool{
 					"pool1": {
+						Name:       "pool1",
 						AutoAssign: true,
 						CIDR: []*net.IPNet{
 							ipnet("3.3.3.2/31"),
@@ -845,7 +1057,7 @@ func TestParse(t *testing.T) {
 							},
 						},
 					},
-				},
+				}},
 				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
@@ -1235,8 +1447,9 @@ func TestParse(t *testing.T) {
 						BFDProfile:    "default",
 					},
 				},
-				Pools: map[string]*Pool{
+				Pools: &Pools{ByName: map[string]*Pool{
 					"pool1": {
+						Name:       "pool1",
 						AutoAssign: true,
 						CIDR:       []*net.IPNet{ipnet("1.2.3.0/24")},
 						BGPAdvertisements: []*BGPAdvertisement{
@@ -1248,7 +1461,7 @@ func TestParse(t *testing.T) {
 							},
 						},
 					},
-				},
+				}},
 				BFDProfiles: map[string]*BFDProfile{
 					"default": {
 						Name: "default",
@@ -1345,7 +1558,7 @@ func TestParse(t *testing.T) {
 						Password:      "nopass",
 					},
 				},
-				Pools:       map[string]*Pool{},
+				Pools:       &Pools{ByName: map[string]*Pool{}},
 				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
@@ -1473,8 +1686,9 @@ func TestParse(t *testing.T) {
 				},
 			},
 			want: &Config{
-				Pools: map[string]*Pool{
+				Pools: &Pools{ByName: map[string]*Pool{
 					"pool1": {
+						Name:       "pool1",
 						AutoAssign: true,
 						CIDR:       []*net.IPNet{ipnet("1.2.3.0/24")},
 						BGPAdvertisements: []*BGPAdvertisement{
@@ -1486,7 +1700,7 @@ func TestParse(t *testing.T) {
 							},
 						},
 					},
-				},
+				}},
 				BFDProfiles: map[string]*BFDProfile{
 					"nondefault": {
 						Name:             "nondefault",
@@ -1629,8 +1843,9 @@ func TestParse(t *testing.T) {
 						EBGPMultiHop:  true,
 					},
 				},
-				Pools: map[string]*Pool{
+				Pools: &Pools{ByName: map[string]*Pool{
 					"pool1": {
+						Name:          "pool1",
 						CIDR:          []*net.IPNet{ipnet("10.20.0.0/16"), ipnet("10.50.0.0/24")},
 						AvoidBuggyIPs: true,
 						AutoAssign:    false,
@@ -1647,6 +1862,7 @@ func TestParse(t *testing.T) {
 						},
 					},
 					"legacybgppool1": {
+						Name: "legacybgppool1",
 						CIDR: []*net.IPNet{ipnet("10.40.0.0/16"), ipnet("10.60.0.0/24")},
 						BGPAdvertisements: []*BGPAdvertisement{
 							{
@@ -1661,13 +1877,14 @@ func TestParse(t *testing.T) {
 						},
 					},
 					"legacyl2pool1": {
+						Name: "legacyl2pool1",
 						CIDR: []*net.IPNet{ipnet("10.21.0.0/16"), ipnet("10.51.0.0/24")},
 						L2Advertisements: []*L2Advertisement{{
 							Nodes:         map[string]bool{},
 							AllInterfaces: true,
 						}},
 					},
-				},
+				}},
 				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
@@ -1714,8 +1931,9 @@ func TestParse(t *testing.T) {
 				},
 			},
 			want: &Config{
-				Pools: map[string]*Pool{
+				Pools: &Pools{ByName: map[string]*Pool{
 					"legacybgppool1": {
+						Name: "legacybgppool1",
 						CIDR: []*net.IPNet{ipnet("10.40.0.0/16"), ipnet("10.60.0.0/24")},
 						BGPAdvertisements: []*BGPAdvertisement{
 							{
@@ -1729,7 +1947,7 @@ func TestParse(t *testing.T) {
 							},
 						},
 					},
-				},
+				}},
 				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
@@ -1859,8 +2077,9 @@ func TestParse(t *testing.T) {
 				},
 			},
 			want: &Config{
-				Pools: map[string]*Pool{
+				Pools: &Pools{ByName: map[string]*Pool{
 					"pool1": {
+						Name:       "pool1",
 						CIDR:       []*net.IPNet{ipnet("10.20.0.0/16")},
 						AutoAssign: true,
 						BGPAdvertisements: []*BGPAdvertisement{
@@ -1878,6 +2097,7 @@ func TestParse(t *testing.T) {
 						}},
 					},
 					"pool2": {
+						Name:       "pool2",
 						CIDR:       []*net.IPNet{ipnet("30.0.0.0/16")},
 						AutoAssign: true,
 						BGPAdvertisements: []*BGPAdvertisement{
@@ -1891,7 +2111,7 @@ func TestParse(t *testing.T) {
 						},
 						L2Advertisements: nil,
 					},
-				},
+				}},
 				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
@@ -1930,8 +2150,9 @@ func TestParse(t *testing.T) {
 				},
 			},
 			want: &Config{
-				Pools: map[string]*Pool{
+				Pools: &Pools{ByName: map[string]*Pool{
 					"pool1": {
+						Name:       "pool1",
 						CIDR:       []*net.IPNet{ipnet("10.20.0.0/16")},
 						AutoAssign: true,
 						L2Advertisements: []*L2Advertisement{{
@@ -1940,7 +2161,7 @@ func TestParse(t *testing.T) {
 							Interfaces:    []string{"eth0"},
 						}},
 					},
-				},
+				}},
 				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
@@ -2143,20 +2364,22 @@ func TestParse(t *testing.T) {
 				},
 			},
 			want: &Config{
-				Pools: map[string]*Pool{
+				Pools: &Pools{ByName: map[string]*Pool{
 					"pool1": {
+						Name:              "pool1",
 						CIDR:              []*net.IPNet{ipnet("10.20.0.0/16")},
 						AutoAssign:        true,
 						BGPAdvertisements: nil,
 						L2Advertisements:  nil,
 					},
 					"pool2": {
+						Name:              "pool2",
 						CIDR:              []*net.IPNet{ipnet("30.0.0.0/16")},
 						AutoAssign:        true,
 						BGPAdvertisements: nil,
 						L2Advertisements:  nil,
 					},
-				},
+				}},
 				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
@@ -2281,8 +2504,9 @@ func TestParse(t *testing.T) {
 				},
 			},
 			want: &Config{
-				Pools: map[string]*Pool{
+				Pools: &Pools{ByName: map[string]*Pool{
 					"legacybgppool1": {
+						Name: "legacybgppool1",
 						CIDR: []*net.IPNet{ipnet("10.40.0.0/16"), ipnet("10.60.0.0/24")},
 						BGPAdvertisements: []*BGPAdvertisement{
 							{
@@ -2298,6 +2522,7 @@ func TestParse(t *testing.T) {
 					},
 
 					"legacyl2pool1": {
+						Name: "legacyl2pool1",
 						CIDR: []*net.IPNet{ipnet("10.21.0.0/16"), ipnet("10.51.0.0/24")},
 						L2Advertisements: []*L2Advertisement{{
 							Nodes: map[string]bool{
@@ -2309,6 +2534,7 @@ func TestParse(t *testing.T) {
 					},
 
 					"pool1": {
+						Name:       "pool1",
 						CIDR:       []*net.IPNet{ipnet("10.20.0.0/16")},
 						AutoAssign: true,
 						BGPAdvertisements: []*BGPAdvertisement{
@@ -2327,6 +2553,7 @@ func TestParse(t *testing.T) {
 						}},
 					},
 					"pool2": {
+						Name:       "pool2",
 						CIDR:       []*net.IPNet{ipnet("30.0.0.0/16")},
 						AutoAssign: true,
 						BGPAdvertisements: []*BGPAdvertisement{
@@ -2339,7 +2566,7 @@ func TestParse(t *testing.T) {
 						},
 						L2Advertisements: nil,
 					},
-				},
+				}},
 				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
@@ -2550,8 +2777,9 @@ func TestParse(t *testing.T) {
 				},
 			},
 			want: &Config{
-				Pools: map[string]*Pool{
+				Pools: &Pools{ByName: map[string]*Pool{
 					"pool1": {
+						Name:       "pool1",
 						CIDR:       []*net.IPNet{ipnet("10.20.0.0/16")},
 						AutoAssign: true,
 						BGPAdvertisements: []*BGPAdvertisement{
@@ -2573,7 +2801,7 @@ func TestParse(t *testing.T) {
 							AllInterfaces: true,
 						}},
 					},
-				},
+				}},
 				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
@@ -2631,8 +2859,9 @@ func TestParse(t *testing.T) {
 						EBGPMultiHop:  false,
 					},
 				},
-				Pools: map[string]*Pool{
+				Pools: &Pools{ByName: map[string]*Pool{
 					"pool1": {
+						Name:       "pool1",
 						AutoAssign: true,
 						CIDR:       []*net.IPNet{ipnet("1.2.3.0/24")},
 						BGPAdvertisements: []*BGPAdvertisement{
@@ -2646,7 +2875,7 @@ func TestParse(t *testing.T) {
 							},
 						},
 					},
-				},
+				}},
 				BFDProfiles: map[string]*BFDProfile{},
 			},
 		},
diff --git a/internal/k8s/controllers/config_controller_test.go b/internal/k8s/controllers/config_controller_test.go
index 2ff26a3d..e22b0b53 100644
--- a/internal/k8s/controllers/config_controller_test.go
+++ b/internal/k8s/controllers/config_controller_test.go
@@ -195,6 +195,18 @@ func TestNodeEvent(t *testing.T) {
 		g.Expect(err).To(BeNil())
 	}()
 
+	// count for update on namespace events
+	var initialConfigUpdateCount int
+	var wg sync.WaitGroup
+	wg.Add(1)
+	go func() {
+		defer wg.Done()
+		time.Sleep(5 * time.Second)
+		mutex.Lock()
+		initialConfigUpdateCount = configUpdate
+		mutex.Unlock()
+	}()
+	wg.Wait()
 	// test new node event.
 	node := &corev1.Node{
 		ObjectMeta: metav1.ObjectMeta{Name: "test-node"},
@@ -208,7 +220,7 @@ func TestNodeEvent(t *testing.T) {
 		mutex.Lock()
 		defer mutex.Unlock()
 		return configUpdate
-	}, 5*time.Second, 200*time.Millisecond).Should(Equal(1))
+	}, 5*time.Second, 200*time.Millisecond).Should(Equal(initialConfigUpdateCount + 1))
 	g.Eventually(func() int {
 		mutex.Lock()
 		defer mutex.Unlock()
@@ -234,7 +246,7 @@ func TestNodeEvent(t *testing.T) {
 		mutex.Lock()
 		defer mutex.Unlock()
 		return configUpdate
-	}, 5*time.Second, 200*time.Millisecond).Should(Equal(1))
+	}, 5*time.Second, 200*time.Millisecond).Should(Equal(initialConfigUpdateCount + 1))
 	g.Eventually(func() int {
 		mutex.Lock()
 		defer mutex.Unlock()
@@ -260,7 +272,7 @@ func TestNodeEvent(t *testing.T) {
 		mutex.Lock()
 		defer mutex.Unlock()
 		return configUpdate
-	}, 5*time.Second, 200*time.Millisecond).Should(Equal(2))
+	}, 5*time.Second, 200*time.Millisecond).Should(Equal(initialConfigUpdateCount + 2))
 	g.Eventually(func() int {
 		mutex.Lock()
 		defer mutex.Unlock()
diff --git a/internal/k8s/controllers/pool_controller_test.go b/internal/k8s/controllers/pool_controller_test.go
index 56d36782..bbfef327 100644
--- a/internal/k8s/controllers/pool_controller_test.go
+++ b/internal/k8s/controllers/pool_controller_test.go
@@ -97,7 +97,7 @@ func TestPoolController(t *testing.T) {
 
 		cmpOpt := cmpopts.IgnoreUnexported(metallbcfg.Pool{})
 
-		mockHandler := func(l log.Logger, pools map[string]*config.Pool) SyncState {
+		mockHandler := func(l log.Logger, pools *config.Pools) SyncState {
 			if !cmp.Equal(expectedCfg.Pools, pools, cmpOpt) {
 				t.Errorf("test %s failed, handler called with unexpected config: %s", test.desc, cmp.Diff(expectedCfg.Pools, pools, cmpOpt))
 			}
diff --git a/speaker/bgp_controller_test.go b/speaker/bgp_controller_test.go
index 432ae437..5d4e09bf 100644
--- a/speaker/bgp_controller_test.go
+++ b/speaker/bgp_controller_test.go
@@ -267,7 +267,7 @@ func TestBGPSpeaker(t *testing.T) {
 						NodeSelectors: []labels.Selector{labels.Everything()},
 					},
 				},
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
 						BGPAdvertisements: []*config.BGPAdvertisement{
@@ -277,7 +277,7 @@ func TestBGPSpeaker(t *testing.T) {
 							},
 						},
 					},
-				},
+				}},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
 				"1.2.3.4:0": nil,
@@ -575,7 +575,7 @@ func TestBGPSpeaker(t *testing.T) {
 						NodeSelectors: []labels.Selector{labels.Everything()},
 					},
 				},
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
 						BGPAdvertisements: []*config.BGPAdvertisement{
@@ -592,7 +592,7 @@ func TestBGPSpeaker(t *testing.T) {
 							},
 						},
 					},
-				},
+				}},
 			},
 			balancer: "test1",
 			svc: &v1.Service{
@@ -642,7 +642,7 @@ func TestBGPSpeaker(t *testing.T) {
 						NodeSelectors: []labels.Selector{labels.Everything()},
 					},
 				},
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
 						BGPAdvertisements: []*config.BGPAdvertisement{
@@ -659,7 +659,7 @@ func TestBGPSpeaker(t *testing.T) {
 							},
 						},
 					},
-				},
+				}},
 			},
 			balancer: "test1",
 			svc: &v1.Service{
@@ -707,7 +707,7 @@ func TestBGPSpeaker(t *testing.T) {
 						NodeSelectors: []labels.Selector{labels.Everything()},
 					},
 				},
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
 						BGPAdvertisements: []*config.BGPAdvertisement{
@@ -725,7 +725,7 @@ func TestBGPSpeaker(t *testing.T) {
 							},
 						},
 					},
-				},
+				}},
 			},
 			balancer: "test1",
 			svc: &v1.Service{
@@ -781,7 +781,7 @@ func TestBGPSpeaker(t *testing.T) {
 						NodeSelectors: []labels.Selector{labels.Everything()},
 					},
 				},
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
 						BGPAdvertisements: []*config.BGPAdvertisement{
@@ -791,7 +791,7 @@ func TestBGPSpeaker(t *testing.T) {
 							},
 						},
 					},
-				},
+				}},
 			},
 			balancer: "test1",
 			svc: &v1.Service{
@@ -996,7 +996,7 @@ func TestBGPSpeaker(t *testing.T) {
 						NodeSelectors: []labels.Selector{labels.Everything()},
 					},
 				},
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
 						BGPAdvertisements: []*config.BGPAdvertisement{
@@ -1006,7 +1006,7 @@ func TestBGPSpeaker(t *testing.T) {
 							},
 						},
 					},
-				},
+				}},
 			},
 			balancer: "test2",
 			svc: &v1.Service{
@@ -1138,7 +1138,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 						NodeSelectors: []labels.Selector{labels.Everything()},
 					},
 				},
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
 						BGPAdvertisements: []*config.BGPAdvertisement{
@@ -1148,7 +1148,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 							},
 						},
 					},
-				},
+				}},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
 				"1.2.3.4:0": nil,
@@ -1482,7 +1482,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 						NodeSelectors: []labels.Selector{labels.Everything()},
 					},
 				},
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
 						BGPAdvertisements: []*config.BGPAdvertisement{
@@ -1499,7 +1499,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 							},
 						},
 					},
-				},
+				}},
 			},
 			balancer: "test1",
 			svc: &v1.Service{
@@ -1555,7 +1555,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 						NodeSelectors: []labels.Selector{labels.Everything()},
 					},
 				},
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
 						BGPAdvertisements: []*config.BGPAdvertisement{
@@ -1565,7 +1565,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 							},
 						},
 					},
-				},
+				}},
 			},
 			balancer: "test1",
 			svc: &v1.Service{
@@ -1772,7 +1772,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 						NodeSelectors: []labels.Selector{labels.Everything()},
 					},
 				},
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
 						BGPAdvertisements: []*config.BGPAdvertisement{
@@ -1782,7 +1782,7 @@ func TestBGPSpeakerEPSlices(t *testing.T) {
 							},
 						},
 					},
-				},
+				}},
 			},
 			balancer: "test2",
 			svc: &v1.Service{
@@ -1897,7 +1897,7 @@ func TestNodeSelectors(t *testing.T) {
 						NodeSelectors: []labels.Selector{labels.Everything()},
 					},
 				},
-				Pools: pools,
+				Pools: &config.Pools{ByName: pools},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
 				"1.2.3.4:0": nil,
@@ -1919,7 +1919,7 @@ func TestNodeSelectors(t *testing.T) {
 						},
 					},
 				},
-				Pools: pools,
+				Pools: &config.Pools{ByName: pools},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
 				"1.2.3.4:0": nil,
@@ -1970,7 +1970,7 @@ func TestNodeSelectors(t *testing.T) {
 						},
 					},
 				},
-				Pools: pools,
+				Pools: &config.Pools{ByName: pools},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
 				"1.2.3.4:0": nil,
@@ -2009,7 +2009,7 @@ func TestNodeSelectors(t *testing.T) {
 						},
 					},
 				},
-				Pools: pools,
+				Pools: &config.Pools{ByName: pools},
 			},
 			wantAds: map[string][]*bgp.Advertisement{
 				"1.2.3.4:0": nil,
diff --git a/speaker/layer2_controller_test.go b/speaker/layer2_controller_test.go
index cf2874d3..5d308457 100644
--- a/speaker/layer2_controller_test.go
+++ b/speaker/layer2_controller_test.go
@@ -363,12 +363,12 @@ func TestShouldAnnounce(t *testing.T) {
 			desc:     "One service, two endpoints, one host, controller 1 should announce",
 			balancer: "test1",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -412,12 +412,12 @@ func TestShouldAnnounce(t *testing.T) {
 			desc:     "One service, two endpoints, one host, neither endpoint is ready, no controller should announce",
 			balancer: "test1",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -460,12 +460,12 @@ func TestShouldAnnounce(t *testing.T) {
 			desc:     "One service, two endpoints across two hosts, controller2 should announce",
 			balancer: "test1",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -508,7 +508,7 @@ func TestShouldAnnounce(t *testing.T) {
 			desc:     "One service, two endpoints across two hosts, advertisement only on node 1, controller1 should announce",
 			balancer: "test1",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: []*config.L2Advertisement{
@@ -519,7 +519,7 @@ func TestShouldAnnounce(t *testing.T) {
 							},
 						},
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -560,12 +560,12 @@ func TestShouldAnnounce(t *testing.T) {
 		}, {
 			desc: "One service, two endpoints across two hosts, neither endpoint is ready, no controllers should announce",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -607,12 +607,12 @@ func TestShouldAnnounce(t *testing.T) {
 		{
 			desc: "One service, two endpoints across two hosts, controller 2 is not ready, controller 1 should announce",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -656,12 +656,12 @@ func TestShouldAnnounce(t *testing.T) {
 		{
 			desc: "Two services each with two endpoints across across two hosts, controller 1 should announce the second, controller 2 the first",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -731,12 +731,12 @@ func TestShouldAnnounce(t *testing.T) {
 		{
 			desc: "Two services each with two endpoints across across two hosts, one service has an endpoint not ready on controller 2, controller 2 should not announce for the service with the not ready endpoint",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -808,12 +808,12 @@ func TestShouldAnnounce(t *testing.T) {
 		{
 			desc: "Two services each with two endpoints across across two hosts, one service has an endpoint not ready on controller 1, the other service has an endpoint not ready on controller 2. Each controller should announce for the service with the ready endpoint on that controller",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -887,12 +887,12 @@ func TestShouldAnnounce(t *testing.T) {
 		{
 			desc: "One service with three endpoints across across two hosts, controller 2 hosts two endpoints controller 2 should announce for the service",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -938,12 +938,12 @@ func TestShouldAnnounce(t *testing.T) {
 		{
 			desc: "One service with three endpoints across across two hosts, controller 1 hosts two endpoints controller 2 should announce for the service",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -989,12 +989,12 @@ func TestShouldAnnounce(t *testing.T) {
 		{
 			desc: "One service with three endpoints across across two hosts, controller 2 hosts two endpoints, one of which is not ready, controller 1 should announce for the service",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -1042,12 +1042,12 @@ func TestShouldAnnounce(t *testing.T) {
 		{
 			desc: "One service with three endpoints across across two hosts, controller 1 hosts two endpoints, one of which is not ready, controller 2 should announce for the service",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -1095,12 +1095,12 @@ func TestShouldAnnounce(t *testing.T) {
 		{
 			desc: "One service with three endpoints across across two hosts, controller 2 hosts two endpoints, both of which are not ready, controller 1 should announce for the service",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -1161,8 +1161,8 @@ func TestShouldAnnounce(t *testing.T) {
 		for _, svc := range test.svcs {
 			lbIP := net.ParseIP(svc.Status.LoadBalancer.Ingress[0].IP)
 			lbIP_s := lbIP.String()
-			response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "balancer", []net.IP{lbIP}, test.config.Pools["default"], svc, test.eps[lbIP_s])
-			response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "balancer", []net.IP{lbIP}, test.config.Pools["default"], svc, test.eps[lbIP_s])
+			response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "balancer", []net.IP{lbIP}, test.config.Pools.ByName["default"], svc, test.eps[lbIP_s])
+			response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "balancer", []net.IP{lbIP}, test.config.Pools.ByName["default"], svc, test.eps[lbIP_s])
 			if response1 != test.c1ExpectedResult[lbIP_s] {
 				t.Errorf("%q: shouldAnnounce for controller 1 for service %s returned incorrect result, expected '%s', but received '%s'", test.desc, lbIP_s, test.c1ExpectedResult[lbIP_s], response1)
 			}
@@ -1223,12 +1223,12 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 			desc:     "One service, two endpoints, one host, controller 1 should announce",
 			balancer: "test1",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -1277,12 +1277,12 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 		}, {
 			desc: "One service, two endpoints, one host, neither endpoint is ready, no controller should announce",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -1331,12 +1331,12 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 		}, {
 			desc: "One service, two endpoints across two hosts, controller2 should announce",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -1385,12 +1385,12 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 		}, {
 			desc: "One service, two endpoints across two hosts, neither endpoint is ready, no controllers should announce",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -1439,12 +1439,12 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 		}, {
 			desc: "One service, two endpoints across two hosts, controller 2 is not ready, controller 1 should announce",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -1493,12 +1493,12 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 		}, {
 			desc: "Two services each with two endpoints across across two hosts, each controller should announce one service",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -1583,12 +1583,12 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 		}, {
 			desc: "Two services each with two endpoints across across two hosts, one service has an endpoint not ready on controller 2, each controller should announce one service",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -1673,12 +1673,12 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 		}, {
 			desc: "Two services each with two endpoints across across two hosts, one service has an endpoint not ready on controller 1, the other service has an endpoint not ready on controller 2. Each controller should announce for the service with the ready endpoint on that controller",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -1763,12 +1763,12 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 		}, {
 			desc: "One service with three endpoints across across two hosts, controller 2 hosts two endpoints controller 2 should announce for the service",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -1830,12 +1830,12 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 		}, {
 			desc: "One service with three endpoints across across two hosts, controller 1 hosts two endpoints controller 2 should announce for the service",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -1893,12 +1893,12 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 		}, {
 			desc: "One service with three endpoints across across two hosts, controller 2 hosts two endpoints, one of which is not ready, controller 2 should announce for the service",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -1956,12 +1956,12 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 		}, {
 			desc: "One service with three endpoints across across two hosts, controller 1 hosts two endpoints, one of which is not ready, controller 1 should announce for the service",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -2019,12 +2019,12 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 		}, {
 			desc: "One service with three endpoints across across two hosts, controller 2 hosts two endpoints, both of which are not ready, controller 1 should announce for the service",
 			config: &config.Config{
-				Pools: map[string]*config.Pool{
+				Pools: &config.Pools{ByName: map[string]*config.Pool{
 					"default": {
 						CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 						L2Advertisements: advertisementsForNode,
 					},
-				},
+				}},
 			},
 			svcs: []*v1.Service{
 				{
@@ -2096,8 +2096,8 @@ func TestShouldAnnounceEPSlices(t *testing.T) {
 		for _, svc := range test.svcs {
 			lbIP := net.ParseIP(svc.Status.LoadBalancer.Ingress[0].IP)
 			lbIP_s := lbIP.String()
-			response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, test.config.Pools["default"], svc, test.eps[lbIP_s])
-			response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, test.config.Pools["default"], svc, test.eps[lbIP_s])
+			response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, test.config.Pools.ByName["default"], svc, test.eps[lbIP_s])
+			response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, test.config.Pools.ByName["default"], svc, test.eps[lbIP_s])
 			if response1 != test.c1ExpectedResult[lbIP_s] {
 				t.Errorf("%q: shouldAnnounce for controller 1 for service %s returned incorrect result, expected '%s', but received '%s'", test.desc, lbIP_s, test.c1ExpectedResult[lbIP_s], response1)
 			}
@@ -2310,12 +2310,12 @@ func TestShouldAnnounceNodeSelector(t *testing.T) {
 		}
 
 		cfg := config.Config{
-			Pools: map[string]*config.Pool{
+			Pools: &config.Pools{ByName: map[string]*config.Pool{
 				"default": {
 					CIDR:             []*net.IPNet{ipnet("10.20.30.0/24")},
 					L2Advertisements: test.L2Advertisements,
 				},
-			},
+			}},
 		}
 		if c1.SetConfig(l, &cfg) == controllers.SyncStateError {
 			t.Errorf("%q: SetConfig failed", test.desc)
@@ -2333,8 +2333,8 @@ func TestShouldAnnounceNodeSelector(t *testing.T) {
 
 		lbIP := net.ParseIP(svc.Status.LoadBalancer.Ingress[0].IP)
 		lbIP_s := lbIP.String()
-		response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools["default"], &svc, test.eps[lbIP_s])
-		response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools["default"], &svc, test.eps[lbIP_s])
+		response1 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], &svc, test.eps[lbIP_s])
+		response2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools.ByName["default"], &svc, test.eps[lbIP_s])
 		if response1 != test.c1ExpectedResult[lbIP_s] {
 			t.Errorf("%q: shouldAnnounce for controller 1 for service %s returned incorrect result, expected '%s', but received '%s'", test.desc, lbIP_s, test.c1ExpectedResult[lbIP_s], response1)
 		}
@@ -2378,7 +2378,7 @@ func TestClusterPolicy(t *testing.T) {
 	l := log.NewNopLogger()
 
 	cfg := &config.Config{
-		Pools: map[string]*config.Pool{
+		Pools: &config.Pools{ByName: map[string]*config.Pool{
 			"default": {
 				CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
 				L2Advertisements: []*config.L2Advertisement{
@@ -2390,7 +2390,7 @@ func TestClusterPolicy(t *testing.T) {
 					},
 				},
 			},
-		},
+		}},
 	}
 
 	if c1.SetConfig(l, cfg) == controllers.SyncStateError {
@@ -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["default"], svc1, eps1)
-		response2svc1 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools["default"], svc1, eps1)
+		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)
 
-		response1svc2 := c1.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools["default"], svc2, eps2)
-		response2svc2 := c2.protocolHandlers[config.Layer2].ShouldAnnounce(l, "test1", []net.IP{lbIP}, cfg.Pools["default"], svc2, eps2)
+		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)
 
 		// 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 85a36341..98a0b232 100644
--- a/speaker/speaker_test.go
+++ b/speaker/speaker_test.go
@@ -58,11 +58,11 @@ func TestLoadBalancerCreation(t *testing.T) {
 	}
 
 	cfg := &config.Config{
-		Pools: map[string]*config.Pool{
+		Pools: &config.Pools{ByName: map[string]*config.Pool{
 			"default": {
 				CIDR: []*net.IPNet{ipnet("10.20.30.0/24")},
 			},
-		},
+		}},
 	}
 
 	state := c.SetConfig(logger, cfg)