Repository: metallb/metallb
Pull request #1745 (2022-12-15 12:04:51)

Disable manual ip address pool selection
In case we want to have the cluster administrator enforce the multi tenancy configurability of metallb, we need to disable the option for users to pin to a specific pool.
Hence this enhances ip address pool crd to disable manual ip address pool selection when its ip addresses are requested through service metallb.universe.tf/loadBalancerIPs or metallb.universe.tf/address-pool annotation.

Signed-off-by: Periyasamy Palanisamy <pepalani@redhat.com>

fedepaol: lgtm

Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 7518ac9d..f54d7dee 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -261,6 +261,7 @@ jobs:
           EOF
           rm -rf e2etest # we want to make sure we are not running current e2e by mistake
           cd metallb-v0.12.1
+          sed -i -e 's/quay.io\/frrouting\/frr:stable_7.5/frrouting\/frr:v7.5.1/g' e2etest/pkg/frr/container/container.go # replace with frr image from dockerhub since it has official image.
           FOCUS="L2.*should work for ExternalTrafficPolicy=Cluster|BGP.*A service of protocol load balancer should work with.*IPV4 - ExternalTrafficPolicyCluster$|validate FRR running configuration"
           sudo -E env "PATH=$PATH" inv e2etest --skip-docker --use-operator --focus "$FOCUS" -e /tmp/kind_logs
 
diff --git a/controller/service.go b/controller/service.go
index 70707cff..ef1272d7 100644
--- a/controller/service.go
+++ b/controller/service.go
@@ -94,7 +94,7 @@ func (c *controller) convergeBalancer(l log.Logger, key string, svc *v1.Service)
 	if len(lbIPs) != 0 {
 		// This assign is idempotent if the config is consistent,
 		// otherwise it'll fail and tell us why.
-		if err = c.ips.Assign(key, lbIPs, k8salloc.Ports(svc), k8salloc.SharingKey(svc), k8salloc.BackendKey(svc)); err != nil {
+		if err = c.ips.Assign(key, svc, lbIPs, k8salloc.Ports(svc), k8salloc.SharingKey(svc), k8salloc.BackendKey(svc)); err != nil {
 			level.Info(l).Log("event", "clearAssignment", "error", err, "msg", "current IP not allowed by config, clearing")
 			c.clearServiceState(key, svc)
 			lbIPs = []net.IP{}
@@ -198,7 +198,7 @@ func (c *controller) allocateIPs(key string, svc *v1.Service) ([]net.IP, error)
 		if serviceIPFamily != desiredLbIPFamily {
 			return nil, fmt.Errorf("requested loadBalancer IP(s) %q does not match the ipFamily of the service", desiredLbIPs)
 		}
-		if err := c.ips.Assign(key, desiredLbIPs, k8salloc.Ports(svc), k8salloc.SharingKey(svc), k8salloc.BackendKey(svc)); err != nil {
+		if err := c.ips.Assign(key, svc, desiredLbIPs, k8salloc.Ports(svc), k8salloc.SharingKey(svc), k8salloc.BackendKey(svc)); err != nil {
 			return nil, err
 		}
 		return desiredLbIPs, nil
@@ -206,7 +206,7 @@ func (c *controller) allocateIPs(key string, svc *v1.Service) ([]net.IP, error)
 	// Otherwise, did the user ask for a specific pool?
 	desiredPool := svc.Annotations[annotationAddressPool]
 	if desiredPool != "" {
-		ips, err := c.ips.AllocateFromPool(key, serviceIPFamily, desiredPool, k8salloc.Ports(svc), k8salloc.SharingKey(svc), k8salloc.BackendKey(svc))
+		ips, err := c.ips.AllocateFromPool(key, svc, serviceIPFamily, desiredPool, k8salloc.Ports(svc), k8salloc.SharingKey(svc), k8salloc.BackendKey(svc))
 		if err != nil {
 			return nil, err
 		}
diff --git a/internal/allocator/allocator.go b/internal/allocator/allocator.go
index 3fd80de6..08a66f36 100644
--- a/internal/allocator/allocator.go
+++ b/internal/allocator/allocator.go
@@ -72,7 +72,8 @@ func (a *Allocator) SetPools(pools *config.Pools) error {
 	// 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.ByName, alloc.ips) == "" {
+		pool := poolFor(pools.ByName, alloc.ips)
+		if pool == nil {
 			return fmt.Errorf("new config not compatible with assigned IPs: service %q cannot own %q under new config", svc, alloc.ips)
 		}
 	}
@@ -90,9 +91,12 @@ func (a *Allocator) SetPools(pools *config.Pools) error {
 	// Need to rearrange existing pool mappings and counts
 	for svc, alloc := range a.allocated {
 		pool := poolFor(a.pools.ByName, alloc.ips)
-		if pool != alloc.pool {
+		if pool == nil {
+			return fmt.Errorf("can't retrieve new pool for assigned IPs: service %q cannot own %q under new config", svc, alloc.ips)
+		}
+		if pool.Name != alloc.pool {
 			a.Unassign(svc)
-			alloc.pool = pool
+			alloc.pool = pool.Name
 			// Use the internal assign, we know for a fact the IP is
 			// still usable.
 			a.assign(svc, alloc)
@@ -136,20 +140,23 @@ func (a *Allocator) assign(svc string, alloc *alloc) {
 
 // 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 {
+func (a *Allocator) Assign(svcKey string, svc *v1.Service, ips []net.IP, ports []Port, sharingKey, backendKey string) error {
 	pool := poolFor(a.pools.ByName, ips)
-	if pool == "" {
+	if pool == nil {
 		return fmt.Errorf("%q is not allowed in config", ips)
 	}
 	sk := &key{
 		sharing: sharingKey,
 		backend: backendKey,
 	}
+	if !a.isPoolCompatibleWithService(pool, svc) {
+		return fmt.Errorf("pool %s not compatible for ip assignment", pool.Name)
+	}
 	// Check the dual-stack constraints:
 	// - Two addresses
 	// - Different families, ipv4 and ipv6
 	if len(ips) > 2 {
-		return fmt.Errorf("More than two addresses %q", ips)
+		return fmt.Errorf("more than two addresses %q", ips)
 	}
 	if len(ips) == 2 && (ipfamily.ForAddress(ips[0]) == ipfamily.ForAddress(ips[1])) {
 		return fmt.Errorf("%q %q has the same family", ips[0], ips[1])
@@ -159,7 +166,7 @@ func (a *Allocator) Assign(svc string, ips []net.IP, ports []Port, sharingKey, b
 		// Does the IP already have allocs? If so, needs to be the same
 		// sharing key, and have non-overlapping ports. If not, the
 		// proposed IP needs to be allowed by configuration.
-		if err := a.checkSharing(svc, ip.String(), ports, sk); err != nil {
+		if err := a.checkSharing(svcKey, ip.String(), ports, sk); err != nil {
 			return err
 		}
 	}
@@ -170,7 +177,7 @@ func (a *Allocator) Assign(svc string, ips []net.IP, ports []Port, sharingKey, b
 	// an allocation" block above). Unassigning is idempotent, so it's
 	// unconditionally safe to do.
 	alloc := &alloc{
-		pool:  pool,
+		pool:  pool.Name,
 		ips:   ips,
 		ports: make([]Port, len(ports)),
 		key:   *sk,
@@ -178,7 +185,7 @@ func (a *Allocator) Assign(svc string, ips []net.IP, ports []Port, sharingKey, b
 	for i, port := range ports {
 		alloc.ports[i] = port
 	}
-	a.assign(svc, alloc)
+	a.assign(svcKey, alloc)
 	return nil
 }
 
@@ -215,8 +222,8 @@ func (a *Allocator) Unassign(svc string) bool {
 }
 
 // AllocateFromPool assigns an available IP from pool to service.
-func (a *Allocator) AllocateFromPool(svc string, serviceIPFamily ipfamily.Family, poolName string, ports []Port, sharingKey, backendKey string) ([]net.IP, error) {
-	if alloc := a.allocated[svc]; alloc != nil {
+func (a *Allocator) AllocateFromPool(svcKey string, svc *v1.Service, serviceIPFamily ipfamily.Family, poolName string, ports []Port, sharingKey, backendKey string) ([]net.IP, error) {
+	if alloc := a.allocated[svcKey]; alloc != nil {
 		// Handle the case where the svc has already been assigned an IP but from the wrong family.
 		// This "should-not-happen" since the "serviceIPFamily" is an immutable field in services.
 		allocIPsFamily, err := ipfamily.ForAddressesIPs(alloc.ips)
@@ -226,7 +233,7 @@ func (a *Allocator) AllocateFromPool(svc string, serviceIPFamily ipfamily.Family
 		if allocIPsFamily != serviceIPFamily {
 			return nil, fmt.Errorf("IP for wrong family assigned alloc %s service family %s", allocIPsFamily, serviceIPFamily)
 		}
-		if err := a.Assign(svc, alloc.ips, ports, sharingKey, backendKey); err != nil {
+		if err := a.Assign(svcKey, svc, alloc.ips, ports, sharingKey, backendKey); err != nil {
 			return nil, err
 		}
 		return alloc.ips, nil
@@ -253,7 +260,7 @@ func (a *Allocator) AllocateFromPool(svc string, serviceIPFamily ipfamily.Family
 			// Not the right ip-family
 			continue
 		}
-		ip := a.getIPFromCIDR(cidr, pool.AvoidBuggyIPs, svc, ports, sharingKey, backendKey)
+		ip := a.getIPFromCIDR(cidr, pool.AvoidBuggyIPs, svcKey, ports, sharingKey, backendKey)
 		if ip != nil {
 			ips = append(ips, ip)
 			delete(ipfamilySel, cidrIPFamily)
@@ -264,7 +271,7 @@ func (a *Allocator) AllocateFromPool(svc string, serviceIPFamily ipfamily.Family
 		// Woops, run out of IPs :( Fail.
 		return nil, fmt.Errorf("no available IPs in pool %q for %s IPFamily", poolName, serviceIPFamily)
 	}
-	err := a.Assign(svc, ips, ports, sharingKey, backendKey)
+	err := a.Assign(svcKey, svc, ips, ports, sharingKey, backendKey)
 	if err != nil {
 		return nil, err
 	}
@@ -274,14 +281,14 @@ func (a *Allocator) AllocateFromPool(svc string, serviceIPFamily ipfamily.Family
 // Allocate assigns any available and assignable IP to service.
 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 {
+		if err := a.Assign(svcKey, svc, alloc.ips, ports, sharingKey, backendKey); err != nil {
 			return nil, err
 		}
 		return alloc.ips, nil
 	}
 	pinnedPools := a.sortedPoolsForService(svc)
 	for _, pool := range pinnedPools {
-		if ips, err := a.AllocateFromPool(svcKey, serviceIPFamily, pool.Name, ports, sharingKey, backendKey); err == nil {
+		if ips, err := a.AllocateFromPool(svcKey, svc, serviceIPFamily, pool.Name, ports, sharingKey, backendKey); err == nil {
 			return ips, nil
 		}
 	}
@@ -289,7 +296,7 @@ func (a *Allocator) Allocate(svcKey string, svc *v1.Service, serviceIPFamily ipf
 		if !pool.AutoAssign || pool.ServiceAllocations != nil {
 			continue
 		}
-		if ips, err := a.AllocateFromPool(svcKey, serviceIPFamily, pool.Name, ports, sharingKey, backendKey); err == nil {
+		if ips, err := a.AllocateFromPool(svcKey, svc, serviceIPFamily, pool.Name, ports, sharingKey, backendKey); err == nil {
 			return ips, nil
 		}
 	}
@@ -340,11 +347,31 @@ func (a *Allocator) sortedPoolsForService(svc *v1.Service) []*config.Pool {
 	return append(priorityPools, noPriorityPools...)
 }
 
+func (a *Allocator) isPoolCompatibleWithService(p *config.Pool, svc *v1.Service) bool {
+	if p.ServiceAllocations != nil && p.ServiceAllocations.Namespaces.Len() > 0 &&
+		!p.ServiceAllocations.Namespaces.Has(svc.Namespace) {
+		return false
+	}
+	if p.ServiceAllocations != nil && len(p.ServiceAllocations.ServiceSelectors) > 0 {
+		svcLabels := labels.Set(svc.Labels)
+		for _, svcSelector := range p.ServiceAllocations.ServiceSelectors {
+			if svcSelector.Matches(svcLabels) {
+				return true
+			}
+		}
+		return false
+	}
+	return true
+}
+
 // 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.ByName, alloc.ips)
+		pool := poolFor(a.pools.ByName, alloc.ips)
+		if pool != nil {
+			return pool.Name
+		}
 	}
 	return ""
 }
@@ -404,8 +431,8 @@ func poolCount(p *config.Pool) int64 {
 }
 
 // poolFor returns the pool that owns the requested IPs, or "" if none.
-func poolFor(pools map[string]*config.Pool, ips []net.IP) string {
-	for pname, p := range pools {
+func poolFor(pools map[string]*config.Pool, ips []net.IP) *config.Pool {
+	for _, p := range pools {
 		cnt := 0
 		for _, ip := range ips {
 			if p.AvoidBuggyIPs && ipConfusesBuggyFirmwares(ip) {
@@ -419,10 +446,10 @@ func poolFor(pools map[string]*config.Pool, ips []net.IP) string {
 			}
 		}
 		if cnt == len(ips) {
-			return pname
+			return p
 		}
 	}
-	return ""
+	return nil
 }
 
 // ipConfusesBuggyFirmwares returns true if ip is an IPv4 address ending in 0 or 255.


Test patch:
diff --git a/internal/allocator/allocator_test.go b/internal/allocator/allocator_test.go
index df4673d6..f53af192 100644
--- a/internal/allocator/allocator_test.go
+++ b/internal/allocator/allocator_test.go
@@ -12,10 +12,37 @@ import (
 
 	"go.universe.tf/metallb/internal/config"
 	"go.universe.tf/metallb/internal/ipfamily"
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/util/sets"
 
 	ptu "github.com/prometheus/client_golang/prometheus/testutil"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
+var svc = &v1.Service{
+	ObjectMeta: metav1.ObjectMeta{
+		Name: "test-lb-service",
+	},
+	Spec: v1.ServiceSpec{
+		Type: v1.ServiceTypeLoadBalancer,
+		Ports: []v1.ServicePort{
+			{
+				Protocol: v1.ProtocolTCP,
+				Port:     8080,
+			},
+		},
+	},
+}
+
+func selector(s string) labels.Selector {
+	ret, err := labels.Parse(s)
+	if err != nil {
+		panic(err)
+	}
+	return ret
+}
+
 func TestAssignment(t *testing.T) {
 	alloc := New()
 	if err := alloc.SetPools(&config.Pools{ByName: map[string]*config.Pool{
@@ -36,13 +63,34 @@ func TestAssignment(t *testing.T) {
 				ipnet("1000::4:0/120"),
 			},
 		},
+		"test3": {
+			Name:               "test3",
+			AvoidBuggyIPs:      true,
+			AutoAssign:         true,
+			ServiceAllocations: &config.ServiceAllocation{Namespaces: sets.NewString("test-ns1")},
+			CIDR: []*net.IPNet{
+				ipnet("1.2.5.0/24"),
+				ipnet("1000::5:0/120"),
+			},
+		},
+		"test4": {
+			Name:               "test4",
+			AvoidBuggyIPs:      true,
+			AutoAssign:         true,
+			ServiceAllocations: &config.ServiceAllocation{ServiceSelectors: []labels.Selector{selector("team=metallb")}},
+			CIDR: []*net.IPNet{
+				ipnet("1.2.6.0/24"),
+				ipnet("1000::6:0/120"),
+			},
+		},
 	}}); err != nil {
 		t.Fatalf("SetPools: %s", err)
 	}
 
 	tests := []struct {
 		desc       string
-		svc        string
+		svcKey     string
+		svc        *v1.Service
 		ips        []string
 		ports      []Port
 		sharingKey string
@@ -50,67 +98,79 @@ func TestAssignment(t *testing.T) {
 		wantErr    bool
 	}{
 		{
-			desc: "assign s1",
-			svc:  "s1",
-			ips:  []string{"1.2.3.4"},
+			desc:   "assign s1",
+			svcKey: "s1",
+			svc:    svc,
+			ips:    []string{"1.2.3.4"},
 		},
 		{
-			desc: "s1 idempotent reassign",
-			svc:  "s1",
-			ips:  []string{"1.2.3.4"},
+			desc:   "s1 idempotent reassign",
+			svcKey: "s1",
+			svc:    svc,
+			ips:    []string{"1.2.3.4"},
 		},
 		{
 			desc:    "s2 can't grab s1's IP",
-			svc:     "s2",
+			svcKey:  "s2",
+			svc:     svc,
 			ips:     []string{"1.2.3.4"},
 			wantErr: true,
 		},
 		{
-			desc: "s2 can get the other IP",
-			svc:  "s2",
-			ips:  []string{"1.2.3.5"},
+			desc:   "s2 can get the other IP",
+			svcKey: "s2",
+			svc:    svc,
+			ips:    []string{"1.2.3.5"},
 		},
 		{
 			desc:    "s1 now can't grab s2's IP",
-			svc:     "s1",
+			svcKey:  "s1",
+			svc:     svc,
 			ips:     []string{"1.2.3.5"},
 			wantErr: true,
 		},
 		{
-			desc: "s1 frees its IP",
-			svc:  "s1",
-			ips:  []string{},
+			desc:   "s1 frees its IP",
+			svcKey: "s1",
+			svc:    svc,
+			ips:    []string{},
 		},
 		{
-			desc: "s2 can grab s1's former IP",
-			svc:  "s2",
-			ips:  []string{"1.2.3.4"},
+			desc:   "s2 can grab s1's former IP",
+			svcKey: "s2",
+			svc:    svc,
+			ips:    []string{"1.2.3.4"},
 		},
 		{
-			desc: "s1 can now grab s2's former IP",
-			svc:  "s1",
-			ips:  []string{"1.2.3.5"},
+			desc:   "s1 can now grab s2's former IP",
+			svcKey: "s1",
+			svc:    svc,
+			ips:    []string{"1.2.3.5"},
 		},
 		{
 			desc:    "s3 cannot grab a 0 buggy IP",
-			svc:     "s3",
+			svcKey:  "s3",
+			svc:     svc,
 			ips:     []string{"1.2.4.0"},
 			wantErr: true,
 		},
 		{
 			desc:    "s3 cannot grab a 255 buggy IP",
-			svc:     "s3",
+			svcKey:  "s3",
+			svc:     svc,
 			ips:     []string{"1.2.4.255"},
 			wantErr: true,
 		},
 		{
-			desc: "s3 can grab another IP in that pool",
-			svc:  "s3",
-			ips:  []string{"1.2.4.254"},
+			desc:   "s3 can grab another IP in that pool",
+			svcKey: "s3",
+			svc:    svc,
+			ips:    []string{"1.2.4.254"},
 		},
 		{
 			desc:       "s4 takes an IP, with sharing",
-			svc:        "s4",
+			svcKey:     "s4",
+			svc:        svc,
 			ips:        []string{"1.2.4.3"},
 			ports:      ports("tcp/80"),
 			sharingKey: "sharing",
@@ -118,7 +178,8 @@ func TestAssignment(t *testing.T) {
 		},
 		{
 			desc:       "s4 changes its sharing key in place",
-			svc:        "s4",
+			svcKey:     "s4",
+			svc:        svc,
 			ips:        []string{"1.2.4.3"},
 			ports:      ports("tcp/80"),
 			sharingKey: "share",
@@ -126,7 +187,8 @@ func TestAssignment(t *testing.T) {
 		},
 		{
 			desc:       "s3 can't share with s4 (port conflict)",
-			svc:        "s3",
+			svcKey:     "s3",
+			svc:        svc,
 			ips:        []string{"1.2.4.3"},
 			ports:      ports("tcp/80"),
 			sharingKey: "share",
@@ -135,7 +197,8 @@ func TestAssignment(t *testing.T) {
 		},
 		{
 			desc:       "s3 can't share with s4 (wrong sharing key)",
-			svc:        "s3",
+			svcKey:     "s3",
+			svc:        svc,
 			ips:        []string{"1.2.4.3"},
 			ports:      ports("tcp/443"),
 			sharingKey: "othershare",
@@ -144,7 +207,8 @@ func TestAssignment(t *testing.T) {
 		},
 		{
 			desc:       "s3 can't share with s4 (wrong backend key)",
-			svc:        "s3",
+			svcKey:     "s3",
+			svc:        svc,
 			ips:        []string{"1.2.4.3"},
 			ports:      ports("tcp/443"),
 			sharingKey: "share",
@@ -153,7 +217,8 @@ func TestAssignment(t *testing.T) {
 		},
 		{
 			desc:       "s3 takes the same IP as s4",
-			svc:        "s3",
+			svcKey:     "s3",
+			svc:        svc,
 			ips:        []string{"1.2.4.3"},
 			ports:      ports("tcp/443"),
 			sharingKey: "share",
@@ -161,7 +226,8 @@ func TestAssignment(t *testing.T) {
 		},
 		{
 			desc:       "s3 can change its ports while keeping the same IP",
-			svc:        "s3",
+			svcKey:     "s3",
+			svc:        svc,
 			ips:        []string{"1.2.4.3"},
 			ports:      ports("udp/53"),
 			sharingKey: "share",
@@ -169,7 +235,8 @@ func TestAssignment(t *testing.T) {
 		},
 		{
 			desc:       "s3 can't change its sharing key while keeping the same IP",
-			svc:        "s3",
+			svcKey:     "s3",
+			svc:        svc,
 			ips:        []string{"1.2.4.3"},
 			ports:      ports("tcp/443"),
 			sharingKey: "othershare",
@@ -178,7 +245,8 @@ func TestAssignment(t *testing.T) {
 		},
 		{
 			desc:       "s3 can't change its backend key while keeping the same IP",
-			svc:        "s3",
+			svcKey:     "s3",
+			svc:        svc,
 			ips:        []string{"1.2.4.3"},
 			ports:      ports("tcp/443"),
 			sharingKey: "share",
@@ -186,63 +254,74 @@ func TestAssignment(t *testing.T) {
 			wantErr:    true,
 		},
 		{
-			desc: "s4 takes s3's former IP",
-			svc:  "s4",
-			ips:  []string{"1.2.4.254"},
+			desc:   "s4 takes s3's former IP",
+			svcKey: "s4",
+			svc:    svc,
+			ips:    []string{"1.2.4.254"},
 		},
 
 		// IPv6 tests (same as ipv4 but with ipv6 addresses)
 		{
-			desc: "ipv6 assign s1",
-			svc:  "s1",
-			ips:  []string{"1000::4"},
+			desc:   "ipv6 assign s1",
+			svcKey: "s1",
+			svc:    svc,
+			ips:    []string{"1000::4"},
 		},
 		{
-			desc: "s1 idempotent reassign",
-			svc:  "s1",
-			ips:  []string{"1000::4"},
+			desc:   "s1 idempotent reassign",
+			svcKey: "s1",
+			svc:    svc,
+			ips:    []string{"1000::4"},
 		},
 		{
 			desc:    "s2 can't grab s1's IP",
-			svc:     "s2",
+			svcKey:  "s2",
+			svc:     svc,
 			ips:     []string{"1000::4"},
 			wantErr: true,
 		},
 		{
-			desc: "s2 can get the other IP",
-			svc:  "s2",
-			ips:  []string{"1000::4:5"},
+			desc:   "s2 can get the other IP",
+			svcKey: "s2",
+			svc:    svc,
+			ips:    []string{"1000::4:5"},
 		},
 		{
 			desc:    "s1 now can't grab s2's IP",
-			svc:     "s1",
+			svcKey:  "s1",
+			svc:     svc,
 			ips:     []string{"1000::4:5"},
 			wantErr: true,
 		},
 		{
-			desc: "s1 frees its IP",
-			svc:  "s1",
-			ips:  []string{},
+			desc:   "s1 frees its IP",
+			svcKey: "s1",
+			svc:    svc,
+			ips:    []string{},
 		},
 		{
-			desc: "s2 can grab s1's former IP",
-			svc:  "s2",
-			ips:  []string{"1000::4"},
+			desc:   "s2 can grab s1's former IP",
+			svcKey: "s2",
+			svc:    svc,
+			ips:    []string{"1000::4"},
 		},
 		{
-			desc: "s1 can now grab s2's former IP",
-			svc:  "s1",
-			ips:  []string{"1000::4:5"},
+			desc:   "s1 can now grab s2's former IP",
+			svcKey: "s1",
+			svc:    svc,
+			ips:    []string{"1000::4:5"},
 		},
 		// (buggy-IP N/A for ipv6)
 		{
-			desc: "s3 can grab another IP in that pool",
-			svc:  "s3",
-			ips:  []string{"1000::4:ff"},
+			desc:   "s3 can grab another IP in that pool",
+			svcKey: "s3",
+			svc:    svc,
+			ips:    []string{"1000::4:ff"},
 		},
 		{
 			desc:       "s4 takes an IP, with sharing",
-			svc:        "s4",
+			svcKey:     "s4",
+			svc:        svc,
 			ips:        []string{"1000::4:3"},
 			ports:      ports("tcp/80"),
 			sharingKey: "sharing",
@@ -250,7 +329,8 @@ func TestAssignment(t *testing.T) {
 		},
 		{
 			desc:       "s4 changes its sharing key in place",
-			svc:        "s4",
+			svcKey:     "s4",
+			svc:        svc,
 			ips:        []string{"1000::4:3"},
 			ports:      ports("tcp/80"),
 			sharingKey: "share",
@@ -258,7 +338,8 @@ func TestAssignment(t *testing.T) {
 		},
 		{
 			desc:       "s3 can't share with s4 (port conflict)",
-			svc:        "s3",
+			svcKey:     "s3",
+			svc:        svc,
 			ips:        []string{"1000::4:3"},
 			ports:      ports("tcp/80"),
 			sharingKey: "share",
@@ -267,7 +348,8 @@ func TestAssignment(t *testing.T) {
 		},
 		{
 			desc:       "s3 can't share with s4 (wrong sharing key)",
-			svc:        "s3",
+			svcKey:     "s3",
+			svc:        svc,
 			ips:        []string{"1000::4:3"},
 			ports:      ports("tcp/443"),
 			sharingKey: "othershare",
@@ -276,7 +358,8 @@ func TestAssignment(t *testing.T) {
 		},
 		{
 			desc:       "s3 can't share with s4 (wrong backend key)",
-			svc:        "s3",
+			svcKey:     "s3",
+			svc:        svc,
 			ips:        []string{"1000::4:3"},
 			ports:      ports("tcp/443"),
 			sharingKey: "share",
@@ -285,7 +368,8 @@ func TestAssignment(t *testing.T) {
 		},
 		{
 			desc:       "s3 takes the same IP as s4",
-			svc:        "s3",
+			svcKey:     "s3",
+			svc:        svc,
 			ips:        []string{"1000::4:3"},
 			ports:      ports("tcp/443"),
 			sharingKey: "share",
@@ -293,7 +377,8 @@ func TestAssignment(t *testing.T) {
 		},
 		{
 			desc:       "s3 can change its ports while keeping the same IP",
-			svc:        "s3",
+			svcKey:     "s3",
+			svc:        svc,
 			ips:        []string{"1000::4:3"},
 			ports:      ports("udp/53"),
 			sharingKey: "share",
@@ -301,7 +386,8 @@ func TestAssignment(t *testing.T) {
 		},
 		{
 			desc:       "s3 can't change its sharing key while keeping the same IP",
-			svc:        "s3",
+			svcKey:     "s3",
+			svc:        svc,
 			ips:        []string{"1000::4:3"},
 			ports:      ports("tcp/443"),
 			sharingKey: "othershare",
@@ -310,7 +396,8 @@ func TestAssignment(t *testing.T) {
 		},
 		{
 			desc:       "s3 can't change its backend key while keeping the same IP",
-			svc:        "s3",
+			svcKey:     "s3",
+			svc:        svc,
 			ips:        []string{"1000::4:3"},
 			ports:      ports("tcp/443"),
 			sharingKey: "share",
@@ -318,32 +405,126 @@ func TestAssignment(t *testing.T) {
 			wantErr:    true,
 		},
 		{
-			desc: "s4 takes s3's former IP",
-			svc:  "s4",
-			ips:  []string{"1000::4:ff"},
+			desc:   "s4 takes s3's former IP",
+			svcKey: "s4",
+			svc:    svc,
+			ips:    []string{"1000::4:ff"},
 		},
 		// IP dual-stack test
 		{
-			desc: "s2 frees its IP",
-			svc:  "s2",
-			ips:  []string{},
+			desc:   "s2 frees its IP",
+			svcKey: "s2",
+			svc:    svc,
+			ips:    []string{},
 		},
 		{
-			desc: "ip dual-stack assign s1",
-			svc:  "s1",
-			ips:  []string{"1.2.3.4", "1000::4"},
+			desc:   "ip dual-stack assign s1",
+			svcKey: "s1",
+			svc:    svc,
+			ips:    []string{"1.2.3.4", "1000::4"},
 		},
 		{
 			desc:    "ip dual-stack assign s1 from different pools",
-			svc:     "s1",
+			svcKey:  "s1",
+			svc:     svc,
 			ips:     []string{"1.2.4.5", "1000::4"},
 			wantErr: true,
 		},
+
+		// IP Pool compatibility tests
+		{
+			desc:   "attempt to assign ip from pool pinned on same namespace",
+			svcKey: "s1",
+			svc: &v1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "test-ns1",
+					Name:      "s1",
+				},
+				Spec: v1.ServiceSpec{
+					Type: v1.ServiceTypeLoadBalancer,
+					Ports: []v1.ServicePort{
+						{
+							Protocol: v1.ProtocolTCP,
+							Port:     8080,
+						},
+					},
+				},
+			},
+			ips:     []string{"1.2.5.10"},
+			wantErr: false,
+		},
+		{
+			desc:   "attempt to assign ip from pool pinned on different namespace",
+			svcKey: "s1",
+			svc: &v1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "testns-not-exist",
+					Name:      "s1",
+				},
+				Spec: v1.ServiceSpec{
+					Type: v1.ServiceTypeLoadBalancer,
+					Ports: []v1.ServicePort{
+						{
+							Protocol: v1.ProtocolTCP,
+							Port:     8080,
+						},
+					},
+				},
+			},
+			ips:     []string{"1.2.5.10"},
+			wantErr: true,
+		},
+		{
+			desc:   "attempt to assign ip from pool pinned with same service label",
+			svcKey: "s1",
+			svc: &v1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "testns-not-exist",
+					Name:      "s1",
+					Labels:    map[string]string{"team": "metallb"},
+				},
+				Spec: v1.ServiceSpec{
+					Type:     v1.ServiceTypeLoadBalancer,
+					Selector: map[string]string{"team": "metallb"},
+					Ports: []v1.ServicePort{
+						{
+							Protocol: v1.ProtocolTCP,
+							Port:     8080,
+						},
+					},
+				},
+			},
+			ips:     []string{"1.2.6.10"},
+			wantErr: false,
+		},
+		{
+			desc:   "attempt to assign ip from pool pinned with different service label",
+			svcKey: "s1",
+			svc: &v1.Service{
+				ObjectMeta: metav1.ObjectMeta{
+					Namespace: "testns-not-exist",
+					Name:      "s1",
+					Labels:    map[string]string{"team": "red"},
+				},
+				Spec: v1.ServiceSpec{
+					Type:     v1.ServiceTypeLoadBalancer,
+					Selector: map[string]string{"team": "red"},
+					Ports: []v1.ServicePort{
+						{
+							Protocol: v1.ProtocolTCP,
+							Port:     8080,
+						},
+					},
+				},
+			},
+			ips:     []string{"1.2.6.10"},
+			wantErr: true,
+		},
 	}
 
 	for _, test := range tests {
 		if len(test.ips) == 0 {
-			alloc.Unassign(test.svc)
+			alloc.Unassign(test.svcKey)
 			continue
 		}
 		ips := []net.IP{}
@@ -353,23 +534,23 @@ func TestAssignment(t *testing.T) {
 		if len(ips) == 0 {
 			t.Fatalf("invalid IPs %q in test %q", ips, test.desc)
 		}
-		alreadyHasIPs := reflect.DeepEqual(assigned(alloc, test.svc), test.ips)
-		err := alloc.Assign(test.svc, ips, test.ports, test.sharingKey, test.backendKey)
+		alreadyHasIPs := reflect.DeepEqual(assigned(alloc, test.svcKey), test.ips)
+		err := alloc.Assign(test.svcKey, test.svc, ips, test.ports, test.sharingKey, test.backendKey)
 		if test.wantErr {
 			if err == nil {
 				t.Errorf("%q should have caused an error, but did not", test.desc)
-			} else if a := assigned(alloc, test.svc); !alreadyHasIPs && reflect.DeepEqual(a, test.ips) {
-				t.Errorf("%q: Assign(%q, %q) failed, but allocator did record allocation", test.desc, test.svc, test.ips)
+			} else if a := assigned(alloc, test.svcKey); !alreadyHasIPs && reflect.DeepEqual(a, test.ips) {
+				t.Errorf("%q: Assign(%q, %q) failed, but allocator did record allocation", test.desc, test.svcKey, test.ips)
 			}
 
 			continue
 		}
 
 		if err != nil {
-			t.Errorf("%q: Assign(%q, %q): %s", test.desc, test.svc, test.ips, err)
+			t.Errorf("%q: Assign(%q, %q): %s", test.desc, test.svcKey, test.ips, err)
 		}
-		if a := assigned(alloc, test.svc); !reflect.DeepEqual(a, test.ips) {
-			t.Errorf("%q: ran Assign(%q, %q), but allocator has recorded allocation of %q", test.desc, test.svc, test.ips, a)
+		if a := assigned(alloc, test.svcKey); !reflect.DeepEqual(a, test.ips) {
+			t.Errorf("%q: ran Assign(%q, %q), but allocator has recorded allocation of %q", test.desc, test.svcKey, test.ips, a)
 		}
 	}
 }
@@ -429,7 +610,8 @@ func TestPoolAllocation(t *testing.T) {
 
 	tests := []struct {
 		desc       string
-		svc        string
+		svcKey     string
+		svc        *v1.Service
 		ports      []Port
 		sharingKey string
 		unassign   bool
@@ -438,69 +620,81 @@ func TestPoolAllocation(t *testing.T) {
 	}{
 		{
 			desc:     "s1 gets an IPv4",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s2 gets an IPv4",
-			svc:      "s2",
+			svcKey:   "s2",
+			svc:      svc,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s3 gets an IPv4",
-			svc:      "s3",
+			svcKey:   "s3",
+			svc:      svc,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s4 gets an IPv4",
-			svc:      "s4",
+			svcKey:   "s4",
+			svc:      svc,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s5 can't get an IPv4",
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			wantErr:  true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s6 can't get an IPv4",
-			svc:      "s6",
+			svcKey:   "s6",
+			svc:      svc,
 			wantErr:  true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s1 releases its IPv4",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s5 can now grab s1's former IPv4",
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s6 still can't get an IPv4",
-			svc:      "s6",
+			svcKey:   "s6",
+			svc:      svc,
 			wantErr:  true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s5 unassigns in prep for enabling IPv4 sharing",
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:       "s5 enables IPv4 sharing",
-			svc:        "s5",
+			svcKey:     "s5",
+			svc:        svc,
 			ports:      ports("tcp/80"),
 			sharingKey: "share",
 			ipFamily:   ipfamily.IPv4,
 		},
 		{
 			desc:       "s6 can get an IPv4 now, with sharing",
-			svc:        "s6",
+			svcKey:     "s6",
+			svc:        svc,
 			ports:      ports("tcp/443"),
 			sharingKey: "share",
 			ipFamily:   ipfamily.IPv4,
@@ -509,37 +703,43 @@ func TestPoolAllocation(t *testing.T) {
 		// Clear old ipv4 addresses
 		{
 			desc:     "s1 clear old ipv4 address",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s2 clear old ipv4 address",
-			svc:      "s2",
+			svcKey:   "s2",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s3 clear old ipv4 address",
-			svc:      "s3",
+			svcKey:   "s3",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s4 clear old ipv4 address",
-			svc:      "s4",
+			svcKey:   "s4",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s5 clear old ipv4 address",
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s6 clear old ipv4 address",
-			svc:      "s6",
+			svcKey:   "s6",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
@@ -547,69 +747,81 @@ func TestPoolAllocation(t *testing.T) {
 		// IPv6 tests.
 		{
 			desc:     "s1 gets an IP6",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
 			desc:     "s2 gets an IP6",
-			svc:      "s2",
+			svcKey:   "s2",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
 			desc:     "s3 gets an IP6",
-			svc:      "s3",
+			svcKey:   "s3",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
 			desc:     "s4 gets an IP6",
-			svc:      "s4",
+			svcKey:   "s4",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
 			desc:     "s5 can't get an IP6",
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 			wantErr:  true,
 		},
 		{
 			desc:     "s6 can't get an IP6",
-			svc:      "s6",
+			svcKey:   "s6",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 			wantErr:  true,
 		},
 		{
 			desc:     "s1 releases its IP6",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s5 can now grab s1's former IP6",
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
 			desc:     "s6 still can't get an IP6",
-			svc:      "s6",
+			svcKey:   "s6",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 			wantErr:  true,
 		},
 		{
 			desc:     "s5 unassigns in prep for enabling IP6 sharing",
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:       "s5 enables IP6 sharing",
-			svc:        "s5",
+			svcKey:     "s5",
+			svc:        svc,
 			ports:      ports("tcp/80"),
 			sharingKey: "share",
 			ipFamily:   ipfamily.IPv6,
 		},
 		{
 			desc:       "s6 can get an IP6 now, with sharing",
-			svc:        "s6",
+			svcKey:     "s6",
+			svc:        svc,
 			ports:      ports("tcp/443"),
 			sharingKey: "share",
 			ipFamily:   ipfamily.IPv6,
@@ -618,55 +830,64 @@ func TestPoolAllocation(t *testing.T) {
 		// Test the "should-not-happen" case where an svc already has a IP from the wrong family
 		{
 			desc:     "s1 clear",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s1 get an IPv4",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s1 get an IPv6",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 			wantErr:  true,
 		},
 		// Clear old ipv6 addresses
 		{
 			desc:     "s1 clear old ipv6 address",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
 			desc:     "s2 clear old ipv6 address",
-			svc:      "s2",
+			svcKey:   "s2",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
 			desc:     "s3 clear old ipv6 address",
-			svc:      "s3",
+			svcKey:   "s3",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
 			desc:     "s4 clear old ipv6 address",
-			svc:      "s4",
+			svcKey:   "s4",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
 			desc:     "s5 clear old ipv6 address",
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
 			desc:     "s6 clear old ipv6 address",
-			svc:      "s6",
+			svcKey:   "s6",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv6,
 		},
@@ -674,69 +895,81 @@ func TestPoolAllocation(t *testing.T) {
 		// Dual-stack tests.
 		{
 			desc:     "s1 gets dual-stack IPs",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			ipFamily: ipfamily.DualStack,
 		},
 		{
 			desc:     "s2 gets dual-stack IPs",
-			svc:      "s2",
+			svcKey:   "s2",
+			svc:      svc,
 			ipFamily: ipfamily.DualStack,
 		},
 		{
 			desc:     "s3 gets dual-stack IPs",
-			svc:      "s3",
+			svcKey:   "s3",
+			svc:      svc,
 			ipFamily: ipfamily.DualStack,
 		},
 		{
 			desc:     "s4 gets dual-stack IPs",
-			svc:      "s4",
+			svcKey:   "s4",
+			svc:      svc,
 			ipFamily: ipfamily.DualStack,
 		},
 		{
 			desc:     "s5 can't get dual-stack IPs",
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			ipFamily: ipfamily.DualStack,
 			wantErr:  true,
 		},
 		{
 			desc:     "s6 can't get dual-stack IPs",
-			svc:      "s6",
+			svcKey:   "s6",
+			svc:      svc,
 			ipFamily: ipfamily.DualStack,
 			wantErr:  true,
 		},
 		{
 			desc:     "s1 releases its dual-stack IPs",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.DualStack,
 		},
 		{
 			desc:     "s5 can now grab s1's former dual-stack IPs",
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			ipFamily: ipfamily.DualStack,
 		},
 		{
 			desc:     "s6 still can't get dual-stack IPs",
-			svc:      "s6",
+			svcKey:   "s6",
+			svc:      svc,
 			ipFamily: ipfamily.DualStack,
 			wantErr:  true,
 		},
 		{
 			desc:     "s5 unassigns in prep for enabling dual-stack IPs sharing",
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.DualStack,
 		},
 		{
 			desc:       "s5 enables dual-stack IP sharing",
-			svc:        "s5",
+			svcKey:     "s5",
+			svc:        svc,
 			ports:      ports("tcp/80"),
 			sharingKey: "share",
 			ipFamily:   ipfamily.DualStack,
 		},
 		{
 			desc:       "s6 can get an dual-stack IPs now, with sharing",
-			svc:        "s6",
+			svcKey:     "s6",
+			svc:        svc,
 			ports:      ports("tcp/443"),
 			sharingKey: "share",
 			ipFamily:   ipfamily.DualStack,
@@ -745,10 +978,10 @@ func TestPoolAllocation(t *testing.T) {
 
 	for _, test := range tests {
 		if test.unassign {
-			alloc.Unassign(test.svc)
+			alloc.Unassign(test.svcKey)
 			continue
 		}
-		ips, err := alloc.AllocateFromPool(test.svc, test.ipFamily, "test", test.ports, test.sharingKey, "")
+		ips, err := alloc.AllocateFromPool(test.svcKey, test.svc, test.ipFamily, "test", test.ports, test.sharingKey, "")
 		if test.wantErr {
 			if err == nil {
 				t.Errorf("%s: should have caused an error, but did not", test.desc)
@@ -757,7 +990,7 @@ func TestPoolAllocation(t *testing.T) {
 			continue
 		}
 		if err != nil {
-			t.Errorf("%s: AllocateFromPool(%q, \"test\"): %s", test.desc, test.svc, err)
+			t.Errorf("%s: AllocateFromPool(%q, \"test\"): %s", test.desc, test.svcKey, err)
 		}
 		validIPs := validIP4s
 		if test.ipFamily == ipfamily.IPv6 {
@@ -773,7 +1006,7 @@ func TestPoolAllocation(t *testing.T) {
 	}
 
 	alloc.Unassign("s5")
-	if _, err := alloc.AllocateFromPool("s5", ipfamily.IPv4, "nonexistentpool", nil, "", ""); err == nil {
+	if _, err := alloc.AllocateFromPool("s5", svc, ipfamily.IPv4, "nonexistentpool", nil, "", ""); err == nil {
 		t.Error("Allocating from non-existent pool succeeded")
 	}
 }
@@ -819,7 +1052,8 @@ func TestAllocation(t *testing.T) {
 	}
 	tests := []struct {
 		desc       string
-		svc        string
+		svcKey     string
+		svc        *v1.Service
 		ports      []Port
 		sharingKey string
 		unassign   bool
@@ -828,58 +1062,68 @@ func TestAllocation(t *testing.T) {
 	}{
 		{
 			desc:     "s1 gets an IPv4",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s2 gets an IPv4",
-			svc:      "s2",
+			svcKey:   "s2",
+			svc:      svc,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s3 gets an IPv4",
-			svc:      "s3",
+			svcKey:   "s3",
+			svc:      svc,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s4 gets an IPv4",
-			svc:      "s4",
+			svcKey:   "s4",
+			svc:      svc,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s5 can't get an IPv4",
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			wantErr:  true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s6 can't get an IPv4",
-			svc:      "s6",
+			svcKey:   "s6",
+			svc:      svc,
 			wantErr:  true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s1 gives up its IPv4",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:       "s5 can now get an IPv4",
-			svc:        "s5",
+			svcKey:     "s5",
+			svc:        svc,
 			ports:      ports("tcp/80"),
 			sharingKey: "share",
 			ipFamily:   ipfamily.IPv4,
 		},
 		{
 			desc:     "s6 still can't get an IPv4",
-			svc:      "s6",
+			svcKey:   "s6",
+			svc:      svc,
 			wantErr:  true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:       "s6 can get an IPv4 with sharing",
-			svc:        "s6",
+			svcKey:     "s6",
+			svc:        svc,
 			ports:      ports("tcp/443"),
 			sharingKey: "share",
 			ipFamily:   ipfamily.IPv4,
@@ -888,37 +1132,43 @@ func TestAllocation(t *testing.T) {
 		// Clear old ipv4 addresses
 		{
 			desc:     "s1 clear old ipv4 address",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s2 clear old ipv4 address",
-			svc:      "s2",
+			svcKey:   "s2",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s3 clear old ipv4 address",
-			svc:      "s3",
+			svcKey:   "s3",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s4 clear old ipv4 address",
-			svc:      "s4",
+			svcKey:   "s4",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s5 clear old ipv4 address",
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
 			desc:     "s6 clear old ipv4 address",
-			svc:      "s6",
+			svcKey:   "s6",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
@@ -926,57 +1176,67 @@ func TestAllocation(t *testing.T) {
 		// IPv6 tests
 		{
 			desc:     "s1 gets an IPv6",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
 			desc:     "s2 gets an IPv6",
-			svc:      "s2",
+			svcKey:   "s2",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
 			desc:     "s3 gets an IPv6",
-			svc:      "s3",
+			svcKey:   "s3",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
 			desc:     "s4 gets an IPv6",
-			svc:      "s4",
+			svcKey:   "s4",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
 			desc:     "s5 can't get an IPv6",
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 			wantErr:  true,
 		},
 		{
 			desc:     "s6 can't get an IPv6",
-			svc:      "s6",
+			svcKey:   "s6",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 			wantErr:  true,
 		},
 		{
 			desc:     "s1 gives up its IPv6",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			unassign: true,
 		},
 		{
 			desc:       "s5 can now get an IPv6",
-			svc:        "s5",
+			svcKey:     "s5",
+			svc:        svc,
 			ports:      ports("tcp/80"),
 			sharingKey: "share",
 			ipFamily:   ipfamily.IPv6,
 		},
 		{
 			desc:     "s6 still can't get an IPv6",
-			svc:      "s6",
+			svcKey:   "s6",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 			wantErr:  true,
 		},
 		{
 			desc:       "s6 can get an IPv6 with sharing",
-			svc:        "s6",
+			svcKey:     "s6",
+			svc:        svc,
 			ports:      ports("tcp/443"),
 			sharingKey: "share",
 			ipFamily:   ipfamily.IPv6,
@@ -984,94 +1244,110 @@ func TestAllocation(t *testing.T) {
 		// Clear old ipv6 addresses
 		{
 			desc:     "s1 clear old ipv6 address",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
 			desc:     "s2 clear old ipv6 address",
-			svc:      "s2",
+			svcKey:   "s2",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
 			desc:     "s3 clear old ipv6 address",
-			svc:      "s3",
+			svcKey:   "s3",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
 			desc:     "s4 clear old ipv6 address",
-			svc:      "s4",
+			svcKey:   "s4",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
 			desc:     "s5 clear old ipv6 address",
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
 			desc:     "s6 clear old ipv6 address",
-			svc:      "s6",
+			svcKey:   "s6",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv6,
 		},
 		//Dual-stack test cases
 		{
 			desc:     "s1 gets dual-stack IPs",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			ipFamily: ipfamily.DualStack,
 		},
 		{
 			desc:     "s2 gets dual-stack IPs",
-			svc:      "s2",
+			svcKey:   "s2",
+			svc:      svc,
 			ipFamily: ipfamily.DualStack,
 		},
 		{
 			desc:     "s3 gets dual-stack IPs",
-			svc:      "s3",
+			svcKey:   "s3",
+			svc:      svc,
 			ipFamily: ipfamily.DualStack,
 		},
 		{
 			desc:     "s4 gets dual-stack IPs",
-			svc:      "s4",
+			svcKey:   "s4",
+			svc:      svc,
 			ipFamily: ipfamily.DualStack,
 		},
 		{
 			desc:     "s5 can't get dual-stack IPs",
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			ipFamily: ipfamily.DualStack,
 			wantErr:  true,
 		},
 		{
 			desc:     "s6 can't get dual-stack IPs",
-			svc:      "s6",
+			svcKey:   "s6",
+			svc:      svc,
 			ipFamily: ipfamily.DualStack,
 			wantErr:  true,
 		},
 		{
 			desc:     "s1 gives up its IPs",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			unassign: true,
 		},
 		{
 			desc:       "s5 can now get dual-stack IPs",
-			svc:        "s5",
+			svcKey:     "s5",
+			svc:        svc,
 			ports:      ports("tcp/80"),
 			sharingKey: "share",
 			ipFamily:   ipfamily.DualStack,
 		},
 		{
 			desc:     "s6 still can't get dual-stack IPs",
-			svc:      "s6",
+			svcKey:   "s6",
+			svc:      svc,
 			ipFamily: ipfamily.DualStack,
 			wantErr:  true,
 		},
 		{
 			desc:       "s6 can get dual-stack IPs with sharing",
-			svc:        "s6",
+			svcKey:     "s6",
+			svc:        svc,
 			ports:      ports("tcp/443"),
 			sharingKey: "share",
 			ipFamily:   ipfamily.DualStack,
@@ -1080,10 +1356,10 @@ func TestAllocation(t *testing.T) {
 
 	for _, test := range tests {
 		if test.unassign {
-			alloc.Unassign(test.svc)
+			alloc.Unassign(test.svcKey)
 			continue
 		}
-		ips, err := alloc.Allocate(test.svc, nil, test.ipFamily, test.ports, test.sharingKey, "")
+		ips, err := alloc.Allocate(test.svcKey, test.svc, 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)
@@ -1091,7 +1367,7 @@ func TestAllocation(t *testing.T) {
 			continue
 		}
 		if err != nil {
-			t.Errorf("%s: Allocate(%q, \"test\"): %s", test.desc, test.svc, err)
+			t.Errorf("%s: Allocate(%q, \"test\"): %s", test.desc, test.svcKey, err)
 		}
 
 		validIPs := validIP4s
@@ -1147,23 +1423,25 @@ func TestBuggyIPs(t *testing.T) {
 	}
 
 	tests := []struct {
-		svc     string
+		svcKey  string
+		svc     *v1.Service
 		wantErr bool
 	}{
-		{svc: "s1"},
-		{svc: "s2"},
-		{svc: "s3"},
-		{svc: "s4"},
-		{svc: "s5"},
-		{svc: "s6"},
-		{
-			svc:     "s7",
+		{svcKey: "s1", svc: svc},
+		{svcKey: "s2", svc: svc},
+		{svcKey: "s3", svc: svc},
+		{svcKey: "s4", svc: svc},
+		{svcKey: "s5", svc: svc},
+		{svcKey: "s6", svc: svc},
+		{
+			svcKey:  "s7",
+			svc:     svc,
 			wantErr: true,
 		},
 	}
 
 	for i, test := range tests {
-		ips, err := alloc.Allocate(test.svc, nil, ipfamily.IPv4, nil, "", "")
+		ips, err := alloc.Allocate(test.svcKey, test.svc, ipfamily.IPv4, nil, "", "")
 		if test.wantErr {
 			if err == nil {
 				t.Errorf("#%d should have caused an error, but did not", i+1)
@@ -1172,7 +1450,7 @@ func TestBuggyIPs(t *testing.T) {
 			continue
 		}
 		if err != nil {
-			t.Errorf("#%d Allocate(%q, \"test\"): %s", i+1, test.svc, err)
+			t.Errorf("#%d Allocate(%q, \"test\"): %s", i+1, test.svcKey, err)
 		}
 		for _, ip := range ips {
 			if !validIPs[ip.String()] {
@@ -1194,10 +1472,10 @@ func TestConfigReload(t *testing.T) {
 	}}); err != nil {
 		t.Fatalf("SetPools: %s", err)
 	}
-	if err := alloc.Assign("s1", []net.IP{net.ParseIP("1.2.3.0")}, nil, "", ""); err != nil {
+	if err := alloc.Assign("s1", svc, []net.IP{net.ParseIP("1.2.3.0")}, nil, "", ""); err != nil {
 		t.Fatalf("Assign(s1, 1.2.3.0): %s", err)
 	}
-	if err := alloc.Assign("s2", []net.IP{net.ParseIP("1000::")}, nil, "", ""); err != nil {
+	if err := alloc.Assign("s2", svc, []net.IP{net.ParseIP("1000::")}, nil, "", ""); err != nil {
 		t.Fatalf("Assign(s2, 1000::): %s", err)
 	}
 	tests := []struct {
@@ -1412,112 +1690,134 @@ func TestAutoAssign(t *testing.T) {
 		"1000::11": true,
 	}
 	tests := []struct {
-		svc      string
+		svcKey   string
+		svc      *v1.Service
 		wantErr  bool
 		ipFamily ipfamily.Family
 		unassign bool
 	}{
 		{
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
-			svc:      "s2",
+			svcKey:   "s2",
+			svc:      svc,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
-			svc:      "s3",
+			svcKey:   "s3",
+			svc:      svc,
 			wantErr:  true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
-			svc:      "s4",
+			svcKey:   "s4",
+			svc:      svc,
 			wantErr:  true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			wantErr:  true,
 			ipFamily: ipfamily.IPv4,
 		},
 
 		// Clear old ipv4 addresses
 		{
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
-			svc:      "s2",
+			svcKey:   "s2",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
-			svc:      "s3",
+			svcKey:   "s3",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
-			svc:      "s4",
+			svcKey:   "s4",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 		{
-			svc:      "s6",
+			svcKey:   "s6",
+			svc:      svc,
 			unassign: true,
 			ipFamily: ipfamily.IPv4,
 		},
 
 		// IPv6 tests;
 		{
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
-			svc:      "s2",
+			svcKey:   "s2",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 		},
 		{
-			svc:      "s3",
+			svcKey:   "s3",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 			wantErr:  true,
 		},
 		{
-			svc:      "s4",
+			svcKey:   "s4",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 			wantErr:  true,
 		},
 		{
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			ipFamily: ipfamily.IPv6,
 			wantErr:  true,
 		},
 		// Dual-stack tests;
 		{
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			ipFamily: ipfamily.DualStack,
 		},
 		{
-			svc:      "s2",
+			svcKey:   "s2",
+			svc:      svc,
 			ipFamily: ipfamily.DualStack,
 		},
 		{
-			svc:      "s3",
+			svcKey:   "s3",
+			svc:      svc,
 			wantErr:  true,
 			ipFamily: ipfamily.DualStack,
 		},
 		{
-			svc:      "s4",
+			svcKey:   "s4",
+			svc:      svc,
 			wantErr:  true,
 			ipFamily: ipfamily.DualStack,
 		},
 		{
-			svc:      "s5",
+			svcKey:   "s5",
+			svc:      svc,
 			wantErr:  true,
 			ipFamily: ipfamily.DualStack,
 		},
@@ -1525,10 +1825,10 @@ func TestAutoAssign(t *testing.T) {
 
 	for i, test := range tests {
 		if test.unassign {
-			alloc.Unassign(test.svc)
+			alloc.Unassign(test.svcKey)
 			continue
 		}
-		ips, err := alloc.Allocate(test.svc, nil, test.ipFamily, nil, "", "")
+		ips, err := alloc.Allocate(test.svcKey, test.svc, test.ipFamily, nil, "", "")
 		if test.wantErr {
 			if err == nil {
 				t.Errorf("#%d should have caused an error, but did not", i+1)
@@ -1536,7 +1836,7 @@ func TestAutoAssign(t *testing.T) {
 			continue
 		}
 		if err != nil {
-			t.Errorf("#%d Allocate(%q, \"test\"): %s", i+1, test.svc, err)
+			t.Errorf("#%d Allocate(%q, \"test\"): %s", i+1, test.svcKey, err)
 		}
 		validIPs := validIP4s
 		if test.ipFamily == ipfamily.IPv6 {
@@ -1615,7 +1915,8 @@ func TestPoolMetrics(t *testing.T) {
 
 	tests := []struct {
 		desc       string
-		svc        string
+		svcKey     string
+		svc        *v1.Service
 		ips        []string
 		ports      []Port
 		sharingKey string
@@ -1624,29 +1925,34 @@ func TestPoolMetrics(t *testing.T) {
 	}{
 		{
 			desc:     "assign s1",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			ips:      []string{"1.2.3.4"},
 			ipsInUse: 1,
 		},
 		{
 			desc:     "assign s2",
-			svc:      "s2",
+			svcKey:   "s2",
+			svc:      svc,
 			ips:      []string{"1.2.3.5"},
 			ipsInUse: 2,
 		},
 		{
 			desc:     "unassign s1",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			ipsInUse: 1,
 		},
 		{
 			desc:     "unassign s2",
-			svc:      "s2",
+			svcKey:   "s2",
+			svc:      svc,
 			ipsInUse: 0,
 		},
 		{
 			desc:       "assign s1 shared",
-			svc:        "s1",
+			svcKey:     "s1",
+			svc:        svc,
 			ips:        []string{"1.2.3.4"},
 			sharingKey: "key",
 			ports:      ports("tcp/80"),
@@ -1654,7 +1960,8 @@ func TestPoolMetrics(t *testing.T) {
 		},
 		{
 			desc:       "assign s2 shared",
-			svc:        "s2",
+			svcKey:     "s2",
+			svc:        svc,
 			ips:        []string{"1.2.3.4"},
 			sharingKey: "key",
 			ports:      ports("tcp/443"),
@@ -1662,7 +1969,8 @@ func TestPoolMetrics(t *testing.T) {
 		},
 		{
 			desc:       "assign s3 shared",
-			svc:        "s3",
+			svcKey:     "s3",
+			svc:        svc,
 			ips:        []string{"1.2.3.4"},
 			sharingKey: "key",
 			ports:      ports("tcp/23"),
@@ -1670,19 +1978,22 @@ func TestPoolMetrics(t *testing.T) {
 		},
 		{
 			desc:     "unassign s1 shared",
-			svc:      "s1",
+			svcKey:   "s1",
+			svc:      svc,
 			ports:    ports("tcp/80"),
 			ipsInUse: 1,
 		},
 		{
 			desc:     "unassign s2 shared",
-			svc:      "s2",
+			svcKey:   "s2",
+			svc:      svc,
 			ports:    ports("tcp/443"),
 			ipsInUse: 1,
 		},
 		{
 			desc:     "unassign s3 shared",
-			svc:      "s3",
+			svcKey:   "s3",
+			svc:      svc,
 			ports:    ports("tcp/23"),
 			ipsInUse: 0,
 		},
@@ -1697,7 +2008,7 @@ func TestPoolMetrics(t *testing.T) {
 
 	for _, test := range tests {
 		if len(test.ips) == 0 {
-			alloc.Unassign(test.svc)
+			alloc.Unassign(test.svcKey)
 			value := ptu.ToFloat64(stats.poolActive.WithLabelValues("test"))
 			if value != test.ipsInUse {
 				t.Errorf("%v; in-use %v. Expected %v", test.desc, value, test.ipsInUse)
@@ -1712,12 +2023,12 @@ func TestPoolMetrics(t *testing.T) {
 		if len(ips) == 0 {
 			t.Fatalf("invalid IP %q in test %q", test.ips, test.desc)
 		}
-		err := alloc.Assign(test.svc, ips, test.ports, test.sharingKey, test.backendKey)
+		err := alloc.Assign(test.svcKey, test.svc, ips, test.ports, test.sharingKey, test.backendKey)
 		if err != nil {
-			t.Errorf("%q: Assign(%q, %q): %v", test.desc, test.svc, test.ips, err)
+			t.Errorf("%q: Assign(%q, %q): %v", test.desc, test.svcKey, test.ips, err)
 		}
-		if a := assigned(alloc, test.svc); !compareIPs(a, test.ips) {
-			t.Errorf("%q: ran Assign(%q, %q), but allocator has recorded allocation of %q", test.desc, test.svc, test.ips, a)
+		if a := assigned(alloc, test.svcKey); !compareIPs(a, test.ips) {
+			t.Errorf("%q: ran Assign(%q, %q), but allocator has recorded allocation of %q", test.desc, test.svcKey, test.ips, a)
 		}
 		value := ptu.ToFloat64(stats.poolActive.WithLabelValues("test"))
 		if value != test.ipsInUse {