Repository: skupperproject/skupper
Pull request #1804 (2024-11-26 16:30:47)

Individual pod addressing
New feature allowing individual pods matched by a connector with a selector to be directly addressed, and for those to then be directly exposed through a corresponding listener.

grs: Updated cockroachdb example inline with this PR here: https://github.com/grs/skupper-example-cockroachdb/tree/v2

Patch:
diff --git a/api/types/crds/skupper_attached_connector_anchor_crd.yaml b/api/types/crds/skupper_attached_connector_anchor_crd.yaml
index 4e04b14c..3f6287ec 100644
--- a/api/types/crds/skupper_attached_connector_anchor_crd.yaml
+++ b/api/types/crds/skupper_attached_connector_anchor_crd.yaml
@@ -19,6 +19,8 @@ spec:
                   type: string
                 routingKey:
                   type: string
+                exposePodsByName:
+                  type: boolean
                 settings:
                   type: object
                   additionalProperties:
diff --git a/api/types/crds/skupper_connector_crd.yaml b/api/types/crds/skupper_connector_crd.yaml
index 0e95beff..89d0a0f2 100644
--- a/api/types/crds/skupper_connector_crd.yaml
+++ b/api/types/crds/skupper_connector_crd.yaml
@@ -33,6 +33,8 @@ spec:
                   type: string
                 includeNotReady:
                   type: boolean
+                exposePodsByName:
+                  type: boolean
                 settings:
                   type: object
                   additionalProperties:
diff --git a/api/types/crds/skupper_listener_crd.yaml b/api/types/crds/skupper_listener_crd.yaml
index d81010a7..72bed0e1 100644
--- a/api/types/crds/skupper_listener_crd.yaml
+++ b/api/types/crds/skupper_listener_crd.yaml
@@ -25,6 +25,8 @@ spec:
                   type: string
                 type:
                   type: string
+                exposePodsByName:
+                  type: boolean
                 settings:
                   type: object
                   additionalProperties:
diff --git a/cmd/network-console-collector/internal/api/types_gen.go b/cmd/network-console-collector/internal/api/types_gen.go
index d5afe9ef..0a890a81 100644
--- a/cmd/network-console-collector/internal/api/types_gen.go
+++ b/cmd/network-console-collector/internal/api/types_gen.go
@@ -75,6 +75,9 @@ type AddressRecord struct {
 	// EndTime The end time in microseconds of the record in Unix timestamp format.
 	EndTime uint64 `json:"endTime"`
 
+	// HasListener true when there is at least one listener for this address
+	HasListener bool `json:"hasListener"`
+
 	// Identity The unique identifier for the record.
 	Identity string `json:"identity"`
 
diff --git a/cmd/network-console-collector/internal/server/handlers.go b/cmd/network-console-collector/internal/server/handlers.go
index 5ae63afa..7400d69d 100644
--- a/cmd/network-console-collector/internal/server/handlers.go
+++ b/cmd/network-console-collector/internal/server/handlers.go
@@ -198,7 +198,7 @@ func (s *server) Processes(w http.ResponseWriter, r *http.Request) {
 
 // (GET /api/v1alpha1/processes/{id}/)
 func (s *server) ProcessById(w http.ResponseWriter, r *http.Request, id string) {
-	getRecord := fetchAndMap(s.records, views.NewProcessProvider(s.records, s.graph), id)
+	getRecord := fetchAndConditionalMap(s.records, views.NewProcessProvider(s.records, s.graph), id)
 	if err := handleSingle(w, r, &api.ProcessResponse{}, getRecord); err != nil {
 		s.logWriteError(r, err)
 	}
diff --git a/cmd/network-console-collector/internal/server/views/views.go b/cmd/network-console-collector/internal/server/views/views.go
index c5ff952f..33f21a71 100644
--- a/cmd/network-console-collector/internal/server/views/views.go
+++ b/cmd/network-console-collector/internal/server/views/views.go
@@ -282,19 +282,30 @@ func NewProcessSliceProvider(stor store.Interface, graph collector.Graph) func(e
 	return func(entries []store.Entry) []api.ProcessRecord {
 		results := make([]api.ProcessRecord, 0, len(entries))
 		for _, e := range entries {
-			link, ok := e.Record.(vanflow.ProcessRecord)
+			record, ok := e.Record.(vanflow.ProcessRecord)
 			if !ok {
 				continue
 			}
-			results = append(results, provider(link))
+			if result, ok := provider(record); ok {
+				results = append(results, result)
+			}
 		}
 		return results
 	}
 }
 
-func NewProcessProvider(stor store.Interface, graph collector.Graph) func(vanflow.ProcessRecord) api.ProcessRecord {
-	return func(record vanflow.ProcessRecord) api.ProcessRecord {
+func NewProcessProvider(stor store.Interface, graph collector.Graph) func(vanflow.ProcessRecord) (api.ProcessRecord, bool) {
+	return func(record vanflow.ProcessRecord) (api.ProcessRecord, bool) {
 		out := defaultProcess(record.ID)
+
+		if record.Parent == nil {
+			return out, false
+		}
+		site, ok := graph.Site(*record.Parent).GetRecord()
+		if !ok {
+			return out, false
+		}
+		setOpt(&out.ParentName, site.Name)
 		out.StartTime, out.EndTime = vanflowTimes(record.BaseRecord)
 		out.ImageName = record.ImageName
 		out.HostName = record.Hostname
@@ -324,17 +335,25 @@ func NewProcessProvider(stor store.Interface, graph collector.Graph) func(vanflo
 		node := graph.Process(record.ID)
 
 		addresses := make(map[api.AtmarkDelimitedString]struct{})
+		hasConnectors := false
+		hasListeners := false
 		for _, cNode := range node.Connectors() {
-			if address, ok := cNode.Address().GetRecord(); ok {
+			hasConnectors = true
+			addressNode := cNode.Address()
+			if !hasListeners {
+				if len(addressNode.RoutingKey().Listeners()) > 0 {
+					hasListeners = true
+				}
+			}
+			if address, ok := addressNode.GetRecord(); ok {
 				addresses[api.NewAtmarkDelimitedString(address.Name, address.ID, address.Protocol)] = struct{}{}
 			}
 		}
-		if site, ok := node.Parent().GetRecord(); ok {
-			setOpt(&out.ParentName, site.Name)
-		}
 
 		if len(addresses) > 0 {
-			out.ProcessBinding = api.Bound
+			if hasListeners && hasConnectors {
+				out.ProcessBinding = api.Bound
+			}
 			addressList := make([]api.AtmarkDelimitedString, 0, len(addresses))
 			for addr := range addresses {
 				addressList = append(addressList, addr)
@@ -343,7 +362,7 @@ func NewProcessProvider(stor store.Interface, graph collector.Graph) func(vanflo
 			out.Addresses = &addressList
 		}
 
-		return out
+		return out, true
 	}
 }
 
@@ -539,6 +558,7 @@ func NewAddressProvider(stor store.Interface, graph collector.Graph) func(collec
 			ObservedApplicationProtocols: protocols,
 			Name:                         record.Name,
 			ListenerCount:                listenerCt,
+			HasListener:                  listenerCt > 0,
 			ConnectorCount:               connectorCt,
 			IsBound:                      listenerCt > 0 && connectorCt > 0,
 		}
diff --git a/cmd/network-console-collector/spec/openapi.yaml b/cmd/network-console-collector/spec/openapi.yaml
index cf6424ee..5972d6c3 100644
--- a/cmd/network-console-collector/spec/openapi.yaml
+++ b/cmd/network-console-collector/spec/openapi.yaml
@@ -1047,6 +1047,7 @@ components:
             - listenerCount
             - connectorCount
             - isBound
+            - hasListener
           properties:
             name:
               type: string
@@ -1064,6 +1065,9 @@ components:
             isBound:
               type: boolean
               description: true when there are both listeners and connectors configured
+            hasListener:
+              type: boolean
+              description: true when there is at least one listener for this address
     ProcessGroupRecord:
       allOf:
         - $ref: '#/components/schemas/baseRecord'
diff --git a/deployments/helm/crds/skupper_attached_connector_anchor_crd.yaml b/deployments/helm/crds/skupper_attached_connector_anchor_crd.yaml
index 4e04b14c..3f6287ec 100644
--- a/deployments/helm/crds/skupper_attached_connector_anchor_crd.yaml
+++ b/deployments/helm/crds/skupper_attached_connector_anchor_crd.yaml
@@ -19,6 +19,8 @@ spec:
                   type: string
                 routingKey:
                   type: string
+                exposePodsByName:
+                  type: boolean
                 settings:
                   type: object
                   additionalProperties:
diff --git a/deployments/helm/crds/skupper_connector_crd.yaml b/deployments/helm/crds/skupper_connector_crd.yaml
index 0e95beff..89d0a0f2 100644
--- a/deployments/helm/crds/skupper_connector_crd.yaml
+++ b/deployments/helm/crds/skupper_connector_crd.yaml
@@ -33,6 +33,8 @@ spec:
                   type: string
                 includeNotReady:
                   type: boolean
+                exposePodsByName:
+                  type: boolean
                 settings:
                   type: object
                   additionalProperties:
diff --git a/deployments/helm/crds/skupper_listener_crd.yaml b/deployments/helm/crds/skupper_listener_crd.yaml
index d81010a7..72bed0e1 100644
--- a/deployments/helm/crds/skupper_listener_crd.yaml
+++ b/deployments/helm/crds/skupper_listener_crd.yaml
@@ -25,6 +25,8 @@ spec:
                   type: string
                 type:
                   type: string
+                exposePodsByName:
+                  type: boolean
                 settings:
                   type: object
                   additionalProperties:
diff --git a/go.mod b/go.mod
index 001c5fdb..9777f540 100644
--- a/go.mod
+++ b/go.mod
@@ -9,8 +9,11 @@ require (
 	github.com/briandowns/spinner v1.23.0
 	github.com/cenkalti/backoff/v4 v4.3.0
 	github.com/fsnotify/fsnotify v1.7.0
+	github.com/go-openapi/errors v0.20.3
 	github.com/go-openapi/runtime v0.24.1
 	github.com/go-openapi/strfmt v0.21.3
+	github.com/go-openapi/swag v0.22.8
+	github.com/go-openapi/validate v0.22.0
 	github.com/google/go-cmp v0.6.0
 	github.com/google/uuid v1.6.0
 	github.com/gorilla/handlers v1.5.2
@@ -31,11 +34,10 @@ require (
 	golang.org/x/text v0.16.0
 	golang.org/x/time v0.5.0
 	gopkg.in/yaml.v3 v3.0.1
-	gotest.tools v2.2.0+incompatible
+	gotest.tools/v3 v3.5.1
 	k8s.io/api v0.31.0
 	k8s.io/apimachinery v0.31.0
 	k8s.io/client-go v0.31.0
-	sigs.k8s.io/controller-runtime v0.19.0
 	sigs.k8s.io/yaml v1.4.0
 )
 
@@ -53,20 +55,16 @@ require (
 	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
 	github.com/emicklei/go-restful/v3 v3.11.0 // indirect
 	github.com/emirpasic/gods v1.18.1 // indirect
-	github.com/evanphx/json-patch/v5 v5.9.0 // indirect
 	github.com/fatih/color v1.7.0 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
 	github.com/fxamacker/cbor/v2 v2.7.0 // indirect
 	github.com/getkin/kin-openapi v0.124.0 // indirect
 	github.com/go-logr/logr v1.4.2 // indirect
 	github.com/go-openapi/analysis v0.21.2 // indirect
-	github.com/go-openapi/errors v0.20.3 // indirect
 	github.com/go-openapi/jsonpointer v0.20.2 // indirect
 	github.com/go-openapi/jsonreference v0.20.2 // indirect
 	github.com/go-openapi/loads v0.21.1 // indirect
 	github.com/go-openapi/spec v0.20.4 // indirect
-	github.com/go-openapi/swag v0.22.8 // indirect
-	github.com/go-openapi/validate v0.22.0 // indirect
 	github.com/gogo/protobuf v1.3.2 // indirect
 	github.com/golang/protobuf v1.5.4 // indirect
 	github.com/google/gnostic-models v0.6.8 // indirect
@@ -85,6 +83,7 @@ require (
 	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
 	github.com/oklog/ulid v1.3.1 // indirect
+	github.com/onsi/gomega v1.33.1 // indirect
 	github.com/opentracing/opentracing-go v1.2.0 // indirect
 	github.com/perimeterx/marshmallow v1.1.5 // indirect
 	github.com/pkg/errors v0.9.1 // indirect
@@ -103,7 +102,6 @@ require (
 	gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
-	k8s.io/apiextensions-apiserver v0.31.0 // indirect
 	k8s.io/klog/v2 v2.130.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
 	k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
diff --git a/go.sum b/go.sum
index b0cd832f..28a96a06 100644
--- a/go.sum
+++ b/go.sum
@@ -83,7 +83,6 @@ github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZ
 github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
 github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
 github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
-github.com/emicklei/go-restful v2.9.5+incompatible h1:spTtZBk5DYEvbxMVutUuTyh1Ao2r4iyvLdACqsl/Ljk=
 github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
 github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
 github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
@@ -92,8 +91,6 @@ github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FM
 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
 github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
-github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg=
-github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ=
 github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -118,8 +115,6 @@ github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7
 github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU=
 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
-github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
-github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
 github.com/go-openapi/analysis v0.21.2 h1:hXFrOYFHUAMQdu6zwAiKKJHJQ8kqZs1ux/ru1P1wLJU=
 github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY=
 github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
@@ -436,10 +431,6 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
 go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
 go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
 go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
-go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
-go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
-go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
-go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
 golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
@@ -464,8 +455,6 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
-golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc h1:mCRnTeVUjcrhlRmO0VK8a6k6Rrf6TF9htwo2pJVSjIU=
-golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -640,8 +629,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
-gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
 google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
 google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
 google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@@ -720,8 +707,8 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C
 gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
-gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
+gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
+gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
@@ -731,8 +718,6 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
 k8s.io/api v0.20.0/go.mod h1:HyLC5l5eoS/ygQYl1BXBgFzWNlkHiAuyNAbevIn+FKg=
 k8s.io/api v0.31.0 h1:b9LiSjR2ym/SzTOlfMHm1tr7/21aD7fSkqgD/CVJBCo=
 k8s.io/api v0.31.0/go.mod h1:0YiFF+JfFxMM6+1hQei8FY8M7s1Mth+z/q7eF1aJkTE=
-k8s.io/apiextensions-apiserver v0.31.0 h1:fZgCVhGwsclj3qCw1buVXCV6khjRzKC5eCFt24kyLSk=
-k8s.io/apiextensions-apiserver v0.31.0/go.mod h1:b9aMDEYaEe5sdK+1T0KU78ApR/5ZVp4i56VacZYEHxk=
 k8s.io/apimachinery v0.20.0/go.mod h1:WlLqWAHZGg07AeltaI0MV5uk1Omp8xaN0JGLY6gkRpU=
 k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc=
 k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo=
@@ -741,7 +726,6 @@ k8s.io/client-go v0.31.0 h1:QqEJzNjbN2Yv1H79SsS+SWnXkBgVu4Pj3CJQgbx0gI8=
 k8s.io/client-go v0.31.0/go.mod h1:Y9wvC76g4fLjmU0BA+rV+h2cncoadjvjjkkIGoTLcGU=
 k8s.io/code-generator v0.20.0/go.mod h1:UsqdF+VX4PU2g46NC2JRs4gc+IfrctnwHb76RNbWHJg=
 k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
-k8s.io/gengo v0.0.0-20201113003025-83324d819ded h1:JApXBKYyB7l9xx+DK7/+mFjC7A9Bt5A93FPvFD0HIFE=
 k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E=
 k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE=
 k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y=
@@ -757,8 +741,6 @@ k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
-sigs.k8s.io/controller-runtime v0.19.0 h1:nWVM7aq+Il2ABxwiCizrVDSlmDcshi9llbaFbC0ji/Q=
-sigs.k8s.io/controller-runtime v0.19.0/go.mod h1:iRmWllt8IlaLjvTTDLhRBXIEtkCK6hwVBJJsYS9Ajf4=
 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
 sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
 sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw=
diff --git a/pkg/apis/skupper/v2alpha1/types.go b/pkg/apis/skupper/v2alpha1/types.go
index 05147a37..a66bb0f5 100644
--- a/pkg/apis/skupper/v2alpha1/types.go
+++ b/pkg/apis/skupper/v2alpha1/types.go
@@ -395,12 +395,13 @@ type ListenerList struct {
 }
 
 type ListenerSpec struct {
-	RoutingKey     string            `json:"routingKey"`
-	Host           string            `json:"host"`
-	Port           int               `json:"port"`
-	TlsCredentials string            `json:"tlsCredentials,omitempty"`
-	Type           string            `json:"type,omitempty"`
-	Settings       map[string]string `json:"settings,omitempty"`
+	RoutingKey       string            `json:"routingKey"`
+	Host             string            `json:"host"`
+	Port             int               `json:"port"`
+	TlsCredentials   string            `json:"tlsCredentials,omitempty"`
+	Type             string            `json:"type,omitempty"`
+	ExposePodsByName bool              `json:"exposePodsByName,omitempty"`
+	Settings         map[string]string `json:"settings,omitempty"`
 }
 
 type ListenerStatus struct {
@@ -481,16 +482,17 @@ type ConnectorList struct {
 }
 
 type ConnectorSpec struct {
-	RoutingKey      string            `json:"routingKey"`
-	Host            string            `json:"host,omitempty"`
-	Selector        string            `json:"selector,omitempty"`
-	Port            int               `json:"port"`
-	TlsCredentials  string            `json:"tlsCredentials,omitempty"`
-	NoClientAuth    bool              `json:"noClientAuth,omitempty"`
-	VerifyHostname  bool              `json:"verifyHostname,omitempty"`
-	Type            string            `json:"type,omitempty"`
-	IncludeNotReady bool              `json:"includeNotReady,omitempty"`
-	Settings        map[string]string `json:"settings,omitempty"`
+	RoutingKey       string            `json:"routingKey"`
+	Host             string            `json:"host,omitempty"`
+	Selector         string            `json:"selector,omitempty"`
+	Port             int               `json:"port"`
+	TlsCredentials   string            `json:"tlsCredentials,omitempty"`
+	NoClientAuth     bool              `json:"noClientAuth,omitempty"`
+	VerifyHostname   bool              `json:"verifyHostname,omitempty"`
+	Type             string            `json:"type,omitempty"`
+	IncludeNotReady  bool              `json:"includeNotReady,omitempty"`
+	ExposePodsByName bool              `json:"exposePodsByName,omitempty"`
+	Settings         map[string]string `json:"settings,omitempty"`
 }
 
 type PodDetails struct {
@@ -1062,5 +1064,6 @@ type AttachedConnectorAnchorList struct {
 type AttachedConnectorAnchorSpec struct {
 	ConnectorNamespace string            `json:"connectorNamespace"`
 	RoutingKey         string            `json:"routingKey"`
+	ExposePodsByName   bool              `json:"exposePodsByName,omitempty"`
 	Settings           map[string]string `json:"settings,omitempty"`
 }
diff --git a/pkg/kube/site/attached_connector.go b/pkg/kube/site/attached_connector.go
index d47a2189..d7423ad7 100644
--- a/pkg/kube/site/attached_connector.go
+++ b/pkg/kube/site/attached_connector.go
@@ -8,132 +8,11 @@ import (
 	"strings"
 
 	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"github.com/skupperproject/skupper/pkg/kube"
 	"github.com/skupperproject/skupper/pkg/qdr"
 	"github.com/skupperproject/skupper/pkg/site"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-type ExtendedBindings struct {
-	bindings   *site.Bindings
-	connectors map[string]*AttachedConnector
-	controller *kube.Controller
-	site       *Site
-	logger     *slog.Logger
-}
-
-func NewExtendedBindings(controller *kube.Controller, profilePath string) *ExtendedBindings {
-	return &ExtendedBindings{
-		bindings:   site.NewBindings(profilePath),
-		connectors: map[string]*AttachedConnector{},
-		controller: controller,
-		logger: slog.New(slog.Default().Handler()).With(
-			slog.String("component", "kube.site.attached_connector"),
-		),
-	}
-}
-
-func (b *ExtendedBindings) SetListenerConfiguration(configuration site.ListenerConfiguration) {
-	b.bindings.SetListenerConfiguration(configuration)
-}
-
-func (b *ExtendedBindings) SetConnectorConfiguration(configuration site.ConnectorConfiguration) {
-	b.bindings.SetConnectorConfiguration(configuration)
-}
-
-func (b *ExtendedBindings) SetBindingEventHandler(handler site.BindingEventHandler) {
-	b.bindings.SetBindingEventHandler(handler)
-}
-
-func (b *ExtendedBindings) UpdateConnector(name string, connector *skupperv2alpha1.Connector) qdr.ConfigUpdate {
-	return b.bindings.UpdateConnector(name, connector)
-}
-
-func (b *ExtendedBindings) UpdateListener(name string, listener *skupperv2alpha1.Listener) qdr.ConfigUpdate {
-	return b.bindings.UpdateListener(name, listener)
-}
-
-func (b *ExtendedBindings) GetConnector(name string) *skupperv2alpha1.Connector {
-	return b.bindings.GetConnector(name)
-}
-
-func (b *ExtendedBindings) Map(cf site.ConnectorFunction, lf site.ListenerFunction) {
-	b.bindings.Map(cf, lf)
-}
-
-type AttachedConnectorFunction func(*AttachedConnector)
-
-func (b *ExtendedBindings) MapOverAttachedConnectors(cf AttachedConnectorFunction) {
-	for _, value := range b.connectors {
-		cf(value)
-	}
-}
-
-func (b *ExtendedBindings) Apply(config *qdr.RouterConfig) bool {
-	desired := b.bindings.ToBridgeConfig()
-	for _, connector := range b.connectors {
-		connector.updateBridgeConfig(b.bindings.SiteId, &desired)
-	}
-	b.bindings.AddSslProfiles(config)
-	config.UpdateBridgeConfig(desired)
-	config.RemoveUnreferencedSslProfiles()
-	return true //TODO: can optimise by indicating if no change was required
-}
-
-func (b *ExtendedBindings) SetSite(site *Site) {
-	b.bindings.SetSiteId(site.site.GetSiteId())
-	b.site = site
-}
-
-func (b *ExtendedBindings) checkAttachedConnectorAnchor(namespace string, name string, anchor *skupperv2alpha1.AttachedConnectorAnchor) error {
-	connector, ok := b.connectors[name]
-	if !ok {
-		connector = NewAttachedConnector(name, namespace, b)
-		b.connectors[name] = connector
-	}
-	if (anchor == nil && connector.anchorDeleted()) || (anchor != nil && connector.anchorUpdated(anchor)) {
-		if b.site != nil {
-			if err := b.site.updateRouterConfigForGroups(b.site.bindings); err != nil {
-				return connector.configurationError(err)
-			} else {
-				return connector.updateStatus()
-			}
-		}
-	}
-	return nil
-}
-
-func (b *ExtendedBindings) attachedConnectorUpdated(name string, definition *skupperv2alpha1.AttachedConnector) error {
-	connector, ok := b.connectors[name]
-	if !ok {
-		connector = NewAttachedConnector(name, definition.Spec.SiteNamespace, b)
-		b.connectors[name] = connector
-	}
-	if connector.definitionUpdated(definition) {
-		if b.site != nil {
-			if err := b.site.updateRouterConfigForGroups(b.site.bindings); err != nil {
-				return connector.configurationError(err)
-			} else {
-				return connector.updateStatus()
-			}
-		}
-	}
-	return nil
-}
-
-func (b *ExtendedBindings) attachedConnectorDeleted(namespace string, name string) error {
-	if connector, ok := b.connectors[name]; ok && connector.definitionDeleted(namespace) {
-		if b.site != nil {
-			if err := b.site.updateRouterConfigForGroups(b.site.bindings); err != nil {
-				return connector.configurationError(err)
-			} else {
-				return connector.updateStatus()
-			}
-		}
-	}
-	return nil
-}
-
 type AttachedConnector struct {
 	name        string
 	namespace   string
@@ -424,6 +303,6 @@ func (a *AttachedConnector) updateBridgeConfig(siteId string, config *qdr.Bridge
 		},
 	}
 	for _, pod := range a.watcher.pods() {
-		site.UpdateBridgeConfigForConnectorToPod(siteId, connector, pod, config)
+		site.UpdateBridgeConfigForConnectorToPod(siteId, connector, pod, a.anchor.Spec.ExposePodsByName, config)
 	}
 }
diff --git a/pkg/kube/site/bindings.go b/pkg/kube/site/bindings.go
index c470b599..3bb9d3ee 100644
--- a/pkg/kube/site/bindings.go
+++ b/pkg/kube/site/bindings.go
@@ -8,8 +8,6 @@ import (
 
 	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 	"github.com/skupperproject/skupper/pkg/kube"
-	"github.com/skupperproject/skupper/pkg/qdr"
-	"github.com/skupperproject/skupper/pkg/site"
 )
 
 var bindings_logger *slog.Logger
@@ -22,117 +20,8 @@ func init() {
 
 type BindingContext interface {
 	Select(connector *skupperv2alpha1.Connector) TargetSelection
-	Expose(ports *ExposedPortSet)
-	Unexpose(host string)
-}
-
-type BindingAdaptor struct {
-	context   BindingContext
-	mapping   *qdr.PortMapping
-	exposed   ExposedPorts
-	selectors map[string]TargetSelection
-}
-
-func (a *BindingAdaptor) init(context BindingContext, config *qdr.RouterConfig) {
-	a.context = context
-	if a.mapping == nil {
-		a.mapping = qdr.RecoverPortMapping(config)
-	}
-	a.exposed = ExposedPorts{}
-	a.selectors = map[string]TargetSelection{}
-}
-
-func (a *BindingAdaptor) cleanup() {
-	for _, s := range a.selectors {
-		s.Close()
-	}
-}
-
-func (a *BindingAdaptor) ConnectorUpdated(connector *skupperv2alpha1.Connector) bool {
-	if selector, ok := a.selectors[connector.Name]; ok {
-		if selector.Selector() == connector.Spec.Selector {
-			// don't need to change the pod watcher, but may need to reconfigure for other change to spec
-			return true
-		} else {
-			// selector has changed so need to close current pod watcher
-			selector.Close()
-			if connector.Spec.Selector == "" {
-				// no longer using a selector, so just delete the old watcher
-				delete(a.selectors, connector.Name)
-				return true
-			}
-			// else create a new watcher below
-		}
-	} else if connector.Spec.Selector == "" {
-		return true
-	}
-	a.selectors[connector.Name] = a.context.Select(connector)
-	// can't yet update configuration; need to wait for the new
-	// watcher to return any matching pods and update config at
-	// that point
-	return false
-}
-
-func (a *BindingAdaptor) ConnectorDeleted(connector *skupperv2alpha1.Connector) {
-	if current, ok := a.selectors[connector.Name]; ok {
-		current.Close()
-		delete(a.selectors, connector.Name)
-	}
-}
-
-func (a *BindingAdaptor) ListenerUpdated(listener *skupperv2alpha1.Listener) {
-	allocatedRouterPort, err := a.mapping.GetPortForKey(listener.Name)
-	if err != nil {
-		bindings_logger.Error("Unable to get port for listener",
-			slog.String("namespace", listener.Namespace),
-			slog.String("name", listener.Name))
-		slog.Any("error", err)
-	} else {
-		port := Port{
-			Name:       listener.Name,
-			Port:       listener.Spec.Port,
-			TargetPort: allocatedRouterPort,
-			Protocol:   listener.Protocol(),
-		}
-		if exposed := a.exposed.Expose(listener.Spec.Host, port); exposed != nil {
-			a.context.Expose(exposed)
-		}
-	}
-}
-
-func (a *BindingAdaptor) ListenerDeleted(listener *skupperv2alpha1.Listener) {
-	a.context.Unexpose(listener.Spec.Host)
-	a.mapping.ReleasePortForKey(listener.Name)
-}
-
-func (a *BindingAdaptor) updateBridgeConfigForConnector(siteId string, connector *skupperv2alpha1.Connector, config *qdr.BridgeConfig) {
-	if connector.Spec.Host != "" {
-		site.UpdateBridgeConfigForConnector(siteId, connector, config)
-	} else if connector.Spec.Selector != "" {
-		if selector, ok := a.selectors[connector.Name]; ok {
-			for _, pod := range selector.List() {
-				site.UpdateBridgeConfigForConnectorToPod(siteId, connector, pod, config)
-			}
-		} else {
-			bindings_logger.Error("Not yet tracking pods for connector with selector set",
-				slog.String("namespace", connector.Namespace),
-				slog.String("name", connector.Name))
-		}
-	} else {
-		bindings_logger.Error("Connector has neither host nor selector set",
-			slog.String("namespace", connector.Namespace),
-			slog.String("name", connector.Name))
-	}
-}
-
-func (a *BindingAdaptor) updateBridgeConfigForListener(siteId string, listener *skupperv2alpha1.Listener, config *qdr.BridgeConfig) {
-	if port, err := a.mapping.GetPortForKey(listener.Name); err == nil {
-		site.UpdateBridgeConfigForListenerWithHostAndPort(siteId, listener, "0.0.0.0", port, config)
-	} else {
-		bindings_logger.Error("Could not allocate port for %s/%s: %s",
-			slog.String("namespace", listener.Namespace),
-			slog.String("name", listener.Name))
-	}
+	Expose(ports *ExposedPortSet) error
+	Unexpose(host string) error
 }
 
 type TargetSelection interface {
diff --git a/pkg/kube/site/extended_bindings.go b/pkg/kube/site/extended_bindings.go
new file mode 100644
index 00000000..8d27c360
--- /dev/null
+++ b/pkg/kube/site/extended_bindings.go
@@ -0,0 +1,310 @@
+package site
+
+import (
+	"errors"
+	"log/slog"
+
+	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
+	"github.com/skupperproject/skupper/pkg/kube"
+	"github.com/skupperproject/skupper/pkg/qdr"
+	"github.com/skupperproject/skupper/pkg/site"
+)
+
+type ExtendedBindings struct {
+	context            BindingContext
+	mapping            *qdr.PortMapping
+	exposed            ExposedPorts
+	selectors          map[string]TargetSelection
+	bindings           *site.Bindings
+	connectors         map[string]*AttachedConnector
+	perTargetListeners map[string]*PerTargetListener
+	controller         *kube.Controller
+	site               *Site
+	logger             *slog.Logger
+}
+
+func NewExtendedBindings(controller *kube.Controller, profilePath string) *ExtendedBindings {
+	eb := &ExtendedBindings{
+		bindings:           site.NewBindings(profilePath),
+		connectors:         map[string]*AttachedConnector{},
+		perTargetListeners: map[string]*PerTargetListener{},
+		controller:         controller,
+		logger: slog.New(slog.Default().Handler()).With(
+			slog.String("component", "kube.site.attached_connector"),
+		),
+	}
+	eb.bindings.SetBindingEventHandler(eb)
+	eb.bindings.SetConnectorConfiguration(eb.updateBridgeConfigForConnector)
+	eb.bindings.SetListenerConfiguration(eb.updateBridgeConfigForListener)
+
+	return eb
+}
+
+func (a *ExtendedBindings) init(context BindingContext, config *qdr.RouterConfig) {
+	a.context = context
+	if a.mapping == nil {
+		a.mapping = qdr.RecoverPortMapping(config)
+	}
+	a.exposed = ExposedPorts{}
+	a.selectors = map[string]TargetSelection{}
+}
+
+func (a *ExtendedBindings) cleanup() {
+	for _, s := range a.selectors {
+		s.Close()
+	}
+}
+
+func (a *ExtendedBindings) ConnectorUpdated(connector *skupperv2alpha1.Connector) bool {
+	if selector, ok := a.selectors[connector.Name]; ok {
+		if selector.Selector() == connector.Spec.Selector {
+			// don't need to change the pod watcher, but may need to reconfigure for other change to spec
+			return true
+		} else {
+			// selector has changed so need to close current pod watcher
+			selector.Close()
+			if connector.Spec.Selector == "" {
+				// no longer using a selector, so just delete the old watcher
+				delete(a.selectors, connector.Name)
+				return true
+			}
+			// else create a new watcher below
+		}
+	} else if connector.Spec.Selector == "" {
+		return true
+	}
+	a.selectors[connector.Name] = a.context.Select(connector)
+	// can't yet update configuration; need to wait for the new
+	// watcher to return any matching pods and update config at
+	// that point
+	return false
+}
+
+func (a *ExtendedBindings) ConnectorDeleted(connector *skupperv2alpha1.Connector) {
+	if current, ok := a.selectors[connector.Name]; ok {
+		current.Close()
+		delete(a.selectors, connector.Name)
+	}
+}
+
+func (a *ExtendedBindings) ListenerUpdated(listener *skupperv2alpha1.Listener) {
+	allocatedRouterPort, err := a.mapping.GetPortForKey(listener.Name)
+	if err != nil {
+		bindings_logger.Error("Unable to get port for listener",
+			slog.String("namespace", listener.Namespace),
+			slog.String("name", listener.Name))
+		slog.Any("error", err)
+	} else {
+		port := Port{
+			Name:       listener.Name,
+			Port:       listener.Spec.Port,
+			TargetPort: allocatedRouterPort,
+			Protocol:   listener.Protocol(),
+		}
+		if exposed := a.exposed.Expose(listener.Spec.Host, port); exposed != nil {
+			if err := a.context.Expose(exposed); err != nil {
+				//TODO: write error to listener status
+			}
+		}
+	}
+}
+
+func (a *ExtendedBindings) ListenerDeleted(listener *skupperv2alpha1.Listener) {
+	if exposed := a.exposed.Unexpose(listener.Spec.Host, listener.Name); exposed != nil {
+		a.mapping.ReleasePortForKey(listener.Name)
+		if exposed.empty() {
+			if err := a.context.Unexpose(listener.Spec.Host); err != nil {
+				//TODO: write error to listener status
+			}
+		} else {
+			if err := a.context.Expose(exposed); err != nil {
+				//TODO: write error to listener status
+			}
+		}
+	}
+}
+
+func (a *ExtendedBindings) updateBridgeConfigForConnector(siteId string, connector *skupperv2alpha1.Connector, config *qdr.BridgeConfig) {
+	if connector.Spec.Host != "" {
+		site.UpdateBridgeConfigForConnector(siteId, connector, config)
+	} else if connector.Spec.Selector != "" {
+		if selector, ok := a.selectors[connector.Name]; ok {
+			for _, pod := range selector.List() {
+				site.UpdateBridgeConfigForConnectorToPod(siteId, connector, pod, connector.Spec.ExposePodsByName, config)
+			}
+		} else {
+			bindings_logger.Error("Not yet tracking pods for connector with selector set",
+				slog.String("namespace", connector.Namespace),
+				slog.String("name", connector.Name))
+		}
+	} else {
+		bindings_logger.Error("Connector has neither host nor selector set",
+			slog.String("namespace", connector.Namespace),
+			slog.String("name", connector.Name))
+	}
+}
+
+func (a *ExtendedBindings) updateBridgeConfigForListener(siteId string, listener *skupperv2alpha1.Listener, config *qdr.BridgeConfig) {
+	if port, err := a.mapping.GetPortForKey(listener.Name); err == nil {
+		site.UpdateBridgeConfigForListenerWithHostAndPort(siteId, listener, "0.0.0.0", port, config)
+	} else {
+		bindings_logger.Error("Could not allocate port for %s/%s: %s",
+			slog.String("namespace", listener.Namespace),
+			slog.String("name", listener.Name))
+	}
+}
+
+func (b *ExtendedBindings) SetListenerConfiguration(configuration site.ListenerConfiguration) {
+	b.bindings.SetListenerConfiguration(configuration)
+}
+
+func (b *ExtendedBindings) SetConnectorConfiguration(configuration site.ConnectorConfiguration) {
+	b.bindings.SetConnectorConfiguration(configuration)
+}
+
+func (b *ExtendedBindings) SetBindingEventHandler(handler site.BindingEventHandler) {
+	b.bindings.SetBindingEventHandler(handler)
+}
+
+func (b *ExtendedBindings) UpdateConnector(name string, connector *skupperv2alpha1.Connector) qdr.ConfigUpdate {
+	return b.bindings.UpdateConnector(name, connector)
+}
+
+func (b *ExtendedBindings) UpdateListener(name string, listener *skupperv2alpha1.Listener) (qdr.ConfigUpdate, error) {
+	var errs []error
+	updateConfig := false
+	if listener != nil && listener.Spec.ExposePodsByName {
+		if existing, ok := b.perTargetListeners[name]; ok {
+			if existing.updateListener(listener) {
+				if err := existing.expose(b.mapping, b.exposed, b.context); err != nil {
+					errs = append(errs, err)
+				}
+				updateConfig = true
+			}
+		} else {
+			b.perTargetListeners[name] = newPerTargetListener(listener)
+		}
+	} else {
+		if existing, ok := b.perTargetListeners[name]; ok {
+			delete(b.perTargetListeners, name)
+			if err := existing.unexposeAll(b.mapping, b.exposed, b.context); err != nil {
+				errs = append(errs, err)
+			}
+
+			updateConfig = true
+		}
+	}
+	if b.bindings.UpdateListener(name, listener) != nil {
+		updateConfig = true
+	}
+	if !updateConfig {
+		return nil, errors.Join(errs...)
+	}
+	return b, errors.Join(errs...)
+}
+
+func (b *ExtendedBindings) GetConnector(name string) *skupperv2alpha1.Connector {
+	return b.bindings.GetConnector(name)
+}
+
+func (b *ExtendedBindings) Map(cf site.ConnectorFunction, lf site.ListenerFunction) {
+	b.bindings.Map(cf, lf)
+}
+
+type AttachedConnectorFunction func(*AttachedConnector)
+
+func (b *ExtendedBindings) MapOverAttachedConnectors(cf AttachedConnectorFunction) {
+	for _, value := range b.connectors {
+		cf(value)
+	}
+}
+
+func (b *ExtendedBindings) Apply(config *qdr.RouterConfig) bool {
+	desired := b.bindings.ToBridgeConfig()
+	for _, connector := range b.connectors {
+		connector.updateBridgeConfig(b.bindings.SiteId, &desired)
+	}
+	for _, ptl := range b.perTargetListeners {
+		ptl.updateBridgeConfig(b.bindings.SiteId, &desired)
+	}
+	b.bindings.AddSslProfiles(config)
+	config.UpdateBridgeConfig(desired)
+	config.RemoveUnreferencedSslProfiles()
+	return true //TODO: can optimise by indicating if no change was required
+}
+
+func (b *ExtendedBindings) SetSite(site *Site) {
+	b.bindings.SetSiteId(site.site.GetSiteId())
+	b.site = site
+}
+
+func (b *ExtendedBindings) checkAttachedConnectorAnchor(namespace string, name string, anchor *skupperv2alpha1.AttachedConnectorAnchor) error {
+	connector, ok := b.connectors[name]
+	if !ok {
+		connector = NewAttachedConnector(name, namespace, b)
+		b.connectors[name] = connector
+	}
+	if (anchor == nil && connector.anchorDeleted()) || (anchor != nil && connector.anchorUpdated(anchor)) {
+		if b.site != nil {
+			if err := b.site.updateRouterConfigForGroups(b.site.bindings); err != nil {
+				return connector.configurationError(err)
+			} else {
+				return connector.updateStatus()
+			}
+		}
+	}
+	return nil
+}
+
+func (b *ExtendedBindings) attachedConnectorUpdated(name string, definition *skupperv2alpha1.AttachedConnector) error {
+	connector, ok := b.connectors[name]
+	if !ok {
+		connector = NewAttachedConnector(name, definition.Spec.SiteNamespace, b)
+		b.connectors[name] = connector
+	}
+	if connector.definitionUpdated(definition) {
+		if b.site != nil {
+			if err := b.site.updateRouterConfigForGroups(b.site.bindings); err != nil {
+				return connector.configurationError(err)
+			} else {
+				return connector.updateStatus()
+			}
+		}
+	}
+	return nil
+}
+
+func (b *ExtendedBindings) attachedConnectorDeleted(namespace string, name string) error {
+	if connector, ok := b.connectors[name]; ok && connector.definitionDeleted(namespace) {
+		if b.site != nil {
+			if err := b.site.updateRouterConfigForGroups(b.site.bindings); err != nil {
+				return connector.configurationError(err)
+			} else {
+				return connector.updateStatus()
+			}
+		}
+	}
+	return nil
+}
+
+func (b *ExtendedBindings) networkUpdated(network []skupperv2alpha1.SiteRecord) qdr.ConfigUpdate {
+	changed := false
+	for _, ptl := range b.perTargetListeners {
+		update, err := ptl.extractTargets(network, b.mapping, b.exposed, b.context)
+		if err != nil {
+			if err := b.site.updateListenerStatus(ptl.definition, err); err != nil {
+				bindings_logger.Error("Error handling network update for listener",
+					slog.String("namespace", ptl.definition.Namespace),
+					slog.String("name", ptl.definition.Name))
+				slog.Any("error", err)
+			}
+		}
+		if update {
+			changed = true
+		}
+	}
+	if !changed {
+		return nil
+	}
+	return b
+}
diff --git a/pkg/kube/site/per_target_listener.go b/pkg/kube/site/per_target_listener.go
new file mode 100644
index 00000000..dfda0a91
--- /dev/null
+++ b/pkg/kube/site/per_target_listener.go
@@ -0,0 +1,144 @@
+package site
+
+import (
+	"reflect"
+	"strconv"
+	"strings"
+
+	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
+	"github.com/skupperproject/skupper/pkg/qdr"
+)
+
+type PerTargetListener struct {
+	definition *skupperv2alpha1.Listener
+	targets    map[string]int // name -> port
+}
+
+func newPerTargetListener(l *skupperv2alpha1.Listener) *PerTargetListener {
+	return &PerTargetListener{
+		definition: l,
+		targets:    map[string]int{},
+	}
+}
+
+func (p *PerTargetListener) updateListener(l *skupperv2alpha1.Listener) bool {
+	changed := p.definition == nil || !reflect.DeepEqual(l.Spec, p.definition.Spec)
+	p.definition = l
+	return changed
+}
+
+func (p *PerTargetListener) extractTargets(network []skupperv2alpha1.SiteRecord, mapping *qdr.PortMapping, exposedPorts ExposedPorts, context BindingContext) (bool, error) {
+	targets := extractTargets(p.address(""), network)
+	changed := false
+	stale := map[string]bool{}
+	for key, _ := range p.targets {
+		stale[key] = true
+	}
+	for _, target := range targets {
+		delete(stale, target)
+		if _, ok := p.targets[target]; !ok {
+			changed = true
+			allocatedRouterPort, err := mapping.GetPortForKey(p.address(target))
+			if err != nil {
+				return false, err
+			}
+			p.targets[target] = allocatedRouterPort
+		}
+	}
+	for target, _ := range stale {
+		mapping.ReleasePortForKey(p.address(target))
+		if err := p.unexpose(target, mapping, exposedPorts, context); err != nil {
+			return false, err
+		}
+		delete(p.targets, target)
+	}
+	return changed, nil
+}
+
+func (p *PerTargetListener) address(target string) string {
+	return p.definition.Spec.RoutingKey + "." + target
+}
+
+func (p *PerTargetListener) expose(mapping *qdr.PortMapping, exposedPorts ExposedPorts, context BindingContext) error {
+	for target, allocatedRouterPort := range p.targets {
+		port := Port{
+			Name:       p.definition.Name,
+			Port:       p.definition.Spec.Port,
+			TargetPort: allocatedRouterPort,
+			Protocol:   p.definition.Protocol(),
+		}
+		if ports := exposedPorts.Expose(target, port); ports != nil {
+			if err := context.Expose(ports); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+func (p *PerTargetListener) unexposeAll(mapping *qdr.PortMapping, exposedPorts ExposedPorts, context BindingContext) error {
+	for target, _ := range p.targets {
+		if err := p.unexpose(target, mapping, exposedPorts, context); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (p *PerTargetListener) unexpose(target string, mapping *qdr.PortMapping, exposedPorts ExposedPorts, context BindingContext) error {
+	if exposed := exposedPorts.Unexpose(target, p.definition.Name); exposed != nil {
+		mapping.ReleasePortForKey(p.address(target))
+		if exposed.empty() {
+			if err := context.Unexpose(target); err != nil {
+				return err
+			}
+		} else {
+			if err := context.Expose(exposed); err != nil {
+				return err
+			}
+		}
+	}
+	return nil
+}
+
+func (p *PerTargetListener) updateBridgeConfig(siteId string, config *qdr.BridgeConfig) {
+	for target, port := range p.targets {
+		if p.definition.Spec.Type == "tcp" || p.definition.Spec.Type == "" {
+			config.AddTcpListener(qdr.TcpEndpoint{
+				Name:       p.definition.Name + "-" + target,
+				SiteId:     siteId,
+				Host:       "0.0.0.0",
+				Port:       strconv.Itoa(port),
+				Address:    p.address(target),
+				SslProfile: p.definition.Spec.TlsCredentials,
+			})
+		}
+	}
+}
+
+func extractTargets(prefix string, network []skupperv2alpha1.SiteRecord) []string {
+	var results []string
+	for _, site := range network {
+		for _, service := range site.Services {
+			if strings.HasPrefix(service.RoutingKey, prefix) {
+				results = append(results, strings.TrimPrefix(service.RoutingKey, prefix))
+			}
+		}
+	}
+	return results
+}
+
+func equivalentSlices(a []string, b []string) bool {
+	if len(a) != len(b) {
+		return false
+	}
+	for _, s := range a {
+		for _, r := range b {
+			if s == r {
+				continue
+			}
+		}
+		return false
+	}
+	return true
+}
diff --git a/pkg/kube/site/ports.go b/pkg/kube/site/ports.go
index e05e59b9..768bc1c8 100644
--- a/pkg/kube/site/ports.go
+++ b/pkg/kube/site/ports.go
@@ -33,6 +33,10 @@ func (p *ExposedPortSet) remove(portname string) bool {
 	return false
 }
 
+func (p *ExposedPortSet) empty() bool {
+	return len(p.Ports) == 0
+}
+
 type ExposedPorts map[string]*ExposedPortSet
 
 func (p ExposedPorts) Expose(host string, port Port) *ExposedPortSet {
@@ -57,6 +61,9 @@ func (p ExposedPorts) Expose(host string, port Port) *ExposedPortSet {
 
 func (p ExposedPorts) Unexpose(host string, portname string) *ExposedPortSet {
 	if existing, ok := p[host]; ok && existing.remove(portname) {
+		if existing.empty() {
+			delete(p, host)
+		}
 		return existing
 	}
 	//no change was required
diff --git a/pkg/kube/site/site.go b/pkg/kube/site/site.go
index 42de6bae..665d1a2f 100644
--- a/pkg/kube/site/site.go
+++ b/pkg/kube/site/site.go
@@ -2,6 +2,7 @@ package site
 
 import (
 	"context"
+	stderrors "errors"
 	"fmt"
 	"log/slog"
 	"reflect"
@@ -40,7 +41,6 @@ type Site struct {
 	linkAccess  site.RouterAccessMap
 	certs       certificates.CertificateManager
 	access      SecuredAccessFactory
-	adaptor     BindingAdaptor
 	routerPods  map[string]*corev1.Pod
 	logger      *slog.Logger
 }
@@ -134,11 +134,8 @@ func (s *Site) reconcile(siteDef *skupperv2alpha1.Site) error {
 			routerConfig = &rc
 		}
 		s.initialised = true
-		s.adaptor.init(s, routerConfig)
+		s.bindings.init(s, routerConfig)
 		s.bindings.SetSite(s)
-		s.bindings.SetBindingEventHandler(&s.adaptor)
-		s.bindings.SetConnectorConfiguration(s.adaptor.updateBridgeConfigForConnector)
-		s.bindings.SetListenerConfiguration(s.adaptor.updateBridgeConfigForListener)
 		if createRouterConfig {
 			s.bindings.Apply(routerConfig)
 			//TODO: apply any recovered RouterAccess configuration
@@ -455,7 +452,7 @@ func (s *Site) WatchPods(context PodWatchingContext, namespace string) *PodWatch
 	return w
 }
 
-func (s *Site) Expose(exposed *ExposedPortSet) {
+func (s *Site) Expose(exposed *ExposedPortSet) error {
 	ctxt := context.TODO()
 	current, err := s.controller.GetKubeClient().CoreV1().Services(s.namespace).Get(ctxt, exposed.Host, metav1.GetOptions{})
 	if errors.IsNotFound(err) {
@@ -488,21 +485,25 @@ func (s *Site) Expose(exposed *ExposedPortSet) {
 					slog.String("service", exposed.Host),
 					slog.String("namespace", s.namespace),
 					slog.Any("error", err))
+				return err
 			} else {
 				s.logger.Info("Created service",
 					slog.String("service", exposed.Host),
 					slog.String("namespace", s.namespace))
+				return nil
 			}
 		} else {
 			s.logger.Info("Did not create service as ports were not updated",
 				slog.String("service", exposed.Host),
 				slog.String("namespace", s.namespace))
+			return nil
 		}
 	} else if err != nil {
 		s.logger.Error("Error checking service",
 			slog.String("service", exposed.Host),
 			slog.String("namespace", s.namespace),
 			slog.Any("error", err))
+		return err
 	} else {
 		updated := false
 		if kube.UpdateSelectorFromMap(&current.Spec, kube.GetLabelsForRouter()) {
@@ -519,16 +520,18 @@ func (s *Site) Expose(exposed *ExposedPortSet) {
 					slog.String("service", exposed.Host),
 					slog.String("namespace", s.namespace),
 					slog.Any("error", err))
+				return err
 			} else {
 				s.logger.Info("Updated service",
 					slog.String("service", exposed.Host),
 					slog.String("namespace", s.namespace))
 			}
 		}
+		return nil
 	}
 }
 
-func (s *Site) Unexpose(name string) {
+func (s *Site) Unexpose(name string) error {
 	ctxt := context.TODO()
 	current, err := s.controller.GetKubeClient().CoreV1().Services(s.namespace).Get(ctxt, name, metav1.GetOptions{})
 	if err != nil {
@@ -538,6 +541,7 @@ func (s *Site) Unexpose(name string) {
 				slog.String("namespace", s.namespace),
 				slog.Any("error", err))
 		}
+		return err
 	} else if isOwned(current) {
 		err = s.controller.GetKubeClient().CoreV1().Services(s.namespace).Delete(ctxt, name, metav1.DeleteOptions{})
 		if err != nil {
@@ -545,9 +549,10 @@ func (s *Site) Unexpose(name string) {
 				slog.String("service", name),
 				slog.String("namespace", s.namespace),
 				slog.Any("error", err))
+			return err
 		}
-		//TODO: ideally error should be propagated back to controller loop
 	}
+	return nil
 }
 
 func isOwned(service *corev1.Service) bool {
@@ -695,15 +700,15 @@ func (s *Site) updateListenerStatus(listener *skupperv2alpha1.Listener, err erro
 }
 
 func (s *Site) CheckListener(name string, listener *skupperv2alpha1.Listener) error {
-	update := s.bindings.UpdateListener(name, listener)
+	update, err1 := s.bindings.UpdateListener(name, listener)
 	if update == nil || s.site == nil {
 		return nil
 	}
-	err := s.updateRouterConfigForGroups(update)
+	err2 := s.updateRouterConfigForGroups(update)
 	if listener == nil {
-		return err
+		return stderrors.Join(err1, err2)
 	}
-	return s.updateListenerStatus(listener, err)
+	return s.updateListenerStatus(listener, stderrors.Join(err1, err2))
 }
 
 func (s *Site) newLink(linkconfig *skupperv2alpha1.Link) *site.Link {
@@ -787,7 +792,7 @@ func (s *Site) Deleted() {
 	s.logger.Info("Deleting site",
 		slog.String("namespace", s.namespace),
 		slog.String("name", s.name))
-	s.adaptor.cleanup()
+	s.bindings.cleanup()
 }
 
 func (s *Site) setDefaultIssuerInStatus() bool {
@@ -868,6 +873,11 @@ func (s *Site) NetworkStatusUpdated(network []skupperv2alpha1.SiteRecord) error
 			}
 		}
 	}
+	if config := s.bindings.networkUpdated(network); config != nil {
+		if err := s.updateRouterConfigForGroups(config); err != nil {
+			return err
+		}
+	}
 
 	bindingStatus := newBindingStatus(s.controller, network)
 	s.bindings.Map(bindingStatus.updateMatchingListenerCount, bindingStatus.updateMatchingConnectorCount)
diff --git a/pkg/site/connector.go b/pkg/site/connector.go
index 7c90a7a8..77073132 100644
--- a/pkg/site/connector.go
+++ b/pkg/site/connector.go
@@ -9,23 +9,25 @@ import (
 
 func UpdateBridgeConfigForConnector(siteId string, connector *skupperv2alpha1.Connector, config *qdr.BridgeConfig) {
 	if connector.Spec.Host != "" {
-		updateBridgeConfigForConnector(siteId, connector, connector.Spec.Host, "", config)
+		updateBridgeConfigForConnector(connector.Name+"-"+connector.Spec.Host, siteId, connector, connector.Spec.Host, "", connector.Spec.RoutingKey, config)
 	}
 }
 
-func UpdateBridgeConfigForConnectorToPod(siteId string, connector *skupperv2alpha1.Connector, pod skupperv2alpha1.PodDetails, config *qdr.BridgeConfig) {
-	updateBridgeConfigForConnector(siteId, connector, pod.IP, pod.UID, config)
+func UpdateBridgeConfigForConnectorToPod(siteId string, connector *skupperv2alpha1.Connector, pod skupperv2alpha1.PodDetails, addQualifiedAddress bool, config *qdr.BridgeConfig) {
+	updateBridgeConfigForConnector(connector.Name+"-"+pod.IP, siteId, connector, pod.IP, pod.UID, connector.Spec.RoutingKey, config)
+	if addQualifiedAddress {
+		updateBridgeConfigForConnector(connector.Name+"-"+pod.Name, siteId, connector, pod.IP, pod.UID, connector.Spec.RoutingKey+"."+pod.Name, config)
+	}
 }
 
-func updateBridgeConfigForConnector(siteId string, connector *skupperv2alpha1.Connector, host string, processID string, config *qdr.BridgeConfig) {
-	name := connector.Name + "-" + host
+func updateBridgeConfigForConnector(name string, siteId string, connector *skupperv2alpha1.Connector, host string, processID string, address string, config *qdr.BridgeConfig) {
 	if connector.Spec.Type == "tcp" || connector.Spec.Type == "" {
 		config.AddTcpConnector(qdr.TcpEndpoint{
 			Name:           name,
 			SiteId:         siteId,
 			Host:           host,
 			Port:           strconv.Itoa(connector.Spec.Port),
-			Address:        connector.Spec.RoutingKey,
+			Address:        address,
 			SslProfile:     getSslProfileName(connector),
 			ProcessID:      processID,
 			VerifyHostname: getVerifyHostname(connector),


Test patch:
diff --git a/api/types/types_test.go b/api/types/types_test.go
index 64683ef8..7162c435 100644
--- a/api/types/types_test.go
+++ b/api/types/types_test.go
@@ -5,7 +5,7 @@ import (
 	"fmt"
 	"testing"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 var jsonDefs = fmt.Sprintf(`[%s, %s]`, jsonDef, jsonDef)
diff --git a/cmd/network-console-collector/internal/collector/addresses_test.go b/cmd/network-console-collector/internal/collector/addresses_test.go
index a575c244..6af8c239 100644
--- a/cmd/network-console-collector/internal/collector/addresses_test.go
+++ b/cmd/network-console-collector/internal/collector/addresses_test.go
@@ -11,7 +11,7 @@ import (
 	"github.com/google/go-cmp/cmp/cmpopts"
 	"github.com/skupperproject/skupper/pkg/vanflow"
 	"github.com/skupperproject/skupper/pkg/vanflow/store"
-	"gotest.tools/poll"
+	"gotest.tools/v3/poll"
 )
 
 func TestAddressManager(t *testing.T) {
diff --git a/cmd/network-console-collector/internal/collector/graph_test.go b/cmd/network-console-collector/internal/collector/graph_test.go
index 2178008b..3810e157 100644
--- a/cmd/network-console-collector/internal/collector/graph_test.go
+++ b/cmd/network-console-collector/internal/collector/graph_test.go
@@ -5,7 +5,7 @@ import (
 
 	"github.com/skupperproject/skupper/pkg/vanflow"
 	"github.com/skupperproject/skupper/pkg/vanflow/store"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestGraphRelations(t *testing.T) {
diff --git a/cmd/network-console-collector/internal/collector/metrics/metrics_test.go b/cmd/network-console-collector/internal/collector/metrics/metrics_test.go
index 7aa35864..7220bad0 100644
--- a/cmd/network-console-collector/internal/collector/metrics/metrics_test.go
+++ b/cmd/network-console-collector/internal/collector/metrics/metrics_test.go
@@ -6,7 +6,7 @@ import (
 	"github.com/prometheus/client_golang/prometheus"
 	prom_testutil "github.com/prometheus/client_golang/prometheus/testutil"
 	"github.com/skupperproject/skupper/pkg/vanflow"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestSiteInfoMetrics(t *testing.T) {
diff --git a/cmd/network-console-collector/internal/collector/pairs_test.go b/cmd/network-console-collector/internal/collector/pairs_test.go
index a12a035e..7de6f09a 100644
--- a/cmd/network-console-collector/internal/collector/pairs_test.go
+++ b/cmd/network-console-collector/internal/collector/pairs_test.go
@@ -10,7 +10,7 @@ import (
 	"github.com/prometheus/client_golang/prometheus"
 	"github.com/skupperproject/skupper/pkg/vanflow"
 	"github.com/skupperproject/skupper/pkg/vanflow/store"
-	"gotest.tools/poll"
+	"gotest.tools/v3/poll"
 )
 
 func TestPairManagerSitePairs(t *testing.T) {
diff --git a/cmd/network-console-collector/internal/server/address_test.go b/cmd/network-console-collector/internal/server/address_test.go
index c2e51a04..bdaaa171 100644
--- a/cmd/network-console-collector/internal/server/address_test.go
+++ b/cmd/network-console-collector/internal/server/address_test.go
@@ -10,7 +10,7 @@ import (
 	"github.com/skupperproject/skupper/cmd/network-console-collector/internal/collector"
 	"github.com/skupperproject/skupper/pkg/vanflow"
 	"github.com/skupperproject/skupper/pkg/vanflow/store"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestAddresses(t *testing.T) {
@@ -52,6 +52,7 @@ func TestAddresses(t *testing.T) {
 							Name:                         "pizza",
 							ConnectorCount:               2,
 							ListenerCount:                2,
+							HasListener:                  true,
 							IsBound:                      true,
 							Protocol:                     "tcp",
 							ObservedApplicationProtocols: []string{"yodel"},
diff --git a/cmd/network-console-collector/internal/server/connections_test.go b/cmd/network-console-collector/internal/server/connections_test.go
index 6ab421e7..f2370734 100644
--- a/cmd/network-console-collector/internal/server/connections_test.go
+++ b/cmd/network-console-collector/internal/server/connections_test.go
@@ -11,7 +11,7 @@ import (
 	"github.com/skupperproject/skupper/cmd/network-console-collector/internal/collector"
 	"github.com/skupperproject/skupper/pkg/vanflow"
 	"github.com/skupperproject/skupper/pkg/vanflow/store"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestConnections(t *testing.T) {
diff --git a/cmd/network-console-collector/internal/server/connector_test.go b/cmd/network-console-collector/internal/server/connector_test.go
index 0c82f135..72240ba0 100644
--- a/cmd/network-console-collector/internal/server/connector_test.go
+++ b/cmd/network-console-collector/internal/server/connector_test.go
@@ -9,7 +9,7 @@ import (
 	"github.com/skupperproject/skupper/cmd/network-console-collector/internal/collector"
 	"github.com/skupperproject/skupper/pkg/vanflow"
 	"github.com/skupperproject/skupper/pkg/vanflow/store"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestConnectors(t *testing.T) {
diff --git a/cmd/network-console-collector/internal/server/links_test.go b/cmd/network-console-collector/internal/server/links_test.go
index 657aaa93..9fb80143 100644
--- a/cmd/network-console-collector/internal/server/links_test.go
+++ b/cmd/network-console-collector/internal/server/links_test.go
@@ -9,7 +9,7 @@ import (
 	"github.com/skupperproject/skupper/cmd/network-console-collector/internal/collector"
 	"github.com/skupperproject/skupper/pkg/vanflow"
 	"github.com/skupperproject/skupper/pkg/vanflow/store"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestRouterlinks(t *testing.T) {
diff --git a/cmd/network-console-collector/internal/server/process_test.go b/cmd/network-console-collector/internal/server/process_test.go
index d9888f73..7ba3601e 100644
--- a/cmd/network-console-collector/internal/server/process_test.go
+++ b/cmd/network-console-collector/internal/server/process_test.go
@@ -9,7 +9,7 @@ import (
 	"github.com/skupperproject/skupper/cmd/network-console-collector/internal/collector"
 	"github.com/skupperproject/skupper/pkg/vanflow"
 	"github.com/skupperproject/skupper/pkg/vanflow/store"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestProcesses(t *testing.T) {
@@ -22,16 +22,28 @@ func TestProcesses(t *testing.T) {
 	testcases := []collectionTestCase[api.ProcessRecord]{
 		{ExpectOK: true},
 		{
-			Records: wrapRecords(
-				vanflow.ProcessRecord{BaseRecord: vanflow.NewBase("1")},
-				vanflow.ProcessRecord{BaseRecord: vanflow.NewBase("2")},
+			Records: wrapRecords( // filters processes with no sites
+				vanflow.ProcessRecord{BaseRecord: vanflow.NewBase("1"), Parent: ptrTo("s1")},
+				vanflow.ProcessRecord{BaseRecord: vanflow.NewBase("2"), Parent: ptrTo("s2")},
 				vanflow.ProcessRecord{BaseRecord: vanflow.NewBase("3")},
 			),
 			ExpectOK:    true,
+			ExpectCount: 0,
+		}, {
+			Records: wrapRecords(
+				vanflow.SiteRecord{BaseRecord: vanflow.NewBase("s1")},
+				vanflow.SiteRecord{BaseRecord: vanflow.NewBase("s2")},
+				vanflow.SiteRecord{BaseRecord: vanflow.NewBase("s3")},
+				vanflow.ProcessRecord{BaseRecord: vanflow.NewBase("1"), Parent: ptrTo("s1")},
+				vanflow.ProcessRecord{BaseRecord: vanflow.NewBase("2"), Parent: ptrTo("s2")},
+				vanflow.ProcessRecord{BaseRecord: vanflow.NewBase("3"), Parent: ptrTo("s3")},
+			),
+			ExpectOK:    true,
 			ExpectCount: 3,
 		}, {
 			Records: wrapRecords(
-				vanflow.ProcessRecord{BaseRecord: vanflow.NewBase("1")},
+				vanflow.SiteRecord{BaseRecord: vanflow.NewBase("s1")},
+				vanflow.ProcessRecord{BaseRecord: vanflow.NewBase("1"), Parent: ptrTo("s1")},
 			),
 			ExpectOK:    true,
 			ExpectCount: 1,
@@ -39,7 +51,7 @@ func TestProcesses(t *testing.T) {
 				r := results[0]
 				assert.DeepEqual(t, r, api.ProcessRecord{
 					Identity:       "1",
-					Parent:         "unknown",
+					Parent:         "s1",
 					ParentName:     "unknown",
 					GroupName:      "unknown",
 					GroupIdentity:  "unknown",
@@ -53,6 +65,31 @@ func TestProcesses(t *testing.T) {
 			Records:     exProcessWithAddresses(),
 			ExpectOK:    true,
 			ExpectCount: 1,
+			ExpectResults: func(t *testing.T, results []api.ProcessRecord) {
+				r := results[0]
+				assert.DeepEqual(t, r, api.ProcessRecord{
+					Identity:       "1",
+					Parent:         "site-1",
+					ParentName:     "site one",
+					Addresses:      ptrTo([]api.AtmarkDelimitedString{api.AtmarkDelimitedString("icecream@icecream-addr-id@tcp"), api.AtmarkDelimitedString("pizza@pizza-addr-id@tcp")}),
+					GroupName:      "group-one",
+					GroupIdentity:  "group-1-id",
+					ProcessBinding: api.Unbound,
+					Name:           "processone",
+					ProcessRole:    api.Internal,
+					SourceHost:     "10.99.99.2",
+				})
+			},
+		}, {
+			Records: append(exProcessWithAddresses(), wrapRecords(
+				vanflow.ListenerRecord{
+					BaseRecord: vanflow.NewBase("l1"),
+					Address:    ptrTo("pizza"),
+					Protocol:   ptrTo("tcp"),
+				},
+			)...),
+			ExpectOK:    true,
+			ExpectCount: 1,
 			ExpectResults: func(t *testing.T, results []api.ProcessRecord) {
 				r := results[0]
 				assert.DeepEqual(t, r, api.ProcessRecord{
@@ -69,7 +106,14 @@ func TestProcesses(t *testing.T) {
 				})
 			},
 		}, {
-			Records:     append(exProcessWithAddresses(), wrapRecords(vanflow.ProcessRecord{BaseRecord: vanflow.NewBase("0")})...),
+			Records: append(exProcessWithAddresses(), wrapRecords(
+				vanflow.ProcessRecord{BaseRecord: vanflow.NewBase("0"), Parent: ptrTo("site-1")},
+				vanflow.ListenerRecord{
+					BaseRecord: vanflow.NewBase("l1"),
+					Address:    ptrTo("pizza"),
+					Protocol:   ptrTo("tcp"),
+				},
+			)...),
 			ExpectOK:    true,
 			ExpectCount: 2,
 			Parameters: map[string][]string{
diff --git a/cmd/network-console-collector/internal/server/processpairs_test.go b/cmd/network-console-collector/internal/server/processpairs_test.go
index 77122f61..5454d85a 100644
--- a/cmd/network-console-collector/internal/server/processpairs_test.go
+++ b/cmd/network-console-collector/internal/server/processpairs_test.go
@@ -8,7 +8,7 @@ import (
 	"github.com/skupperproject/skupper/cmd/network-console-collector/internal/api"
 	"github.com/skupperproject/skupper/cmd/network-console-collector/internal/collector"
 	"github.com/skupperproject/skupper/pkg/vanflow/store"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestProcessPairsByAddress(t *testing.T) {
diff --git a/cmd/network-console-collector/internal/server/router_test.go b/cmd/network-console-collector/internal/server/router_test.go
index 240b7c06..1a36686c 100644
--- a/cmd/network-console-collector/internal/server/router_test.go
+++ b/cmd/network-console-collector/internal/server/router_test.go
@@ -9,7 +9,7 @@ import (
 	"github.com/skupperproject/skupper/cmd/network-console-collector/internal/collector"
 	"github.com/skupperproject/skupper/pkg/vanflow"
 	"github.com/skupperproject/skupper/pkg/vanflow/store"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestRouters(t *testing.T) {
diff --git a/cmd/network-console-collector/internal/server/sites_test.go b/cmd/network-console-collector/internal/server/sites_test.go
index fe87667f..f2ed6b51 100644
--- a/cmd/network-console-collector/internal/server/sites_test.go
+++ b/cmd/network-console-collector/internal/server/sites_test.go
@@ -10,7 +10,7 @@ import (
 	"github.com/skupperproject/skupper/cmd/network-console-collector/internal/collector"
 	"github.com/skupperproject/skupper/pkg/vanflow"
 	"github.com/skupperproject/skupper/pkg/vanflow/store"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestSites(t *testing.T) {
diff --git a/internal/cmd/skupper/common/command_test.go b/internal/cmd/skupper/common/command_test.go
index 3a9f37e2..d26ff6fb 100644
--- a/internal/cmd/skupper/common/command_test.go
+++ b/internal/cmd/skupper/common/command_test.go
@@ -1,10 +1,11 @@
 package common
 
 import (
+	"testing"
+
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/spf13/cobra"
-	"gotest.tools/assert"
-	"testing"
+	"gotest.tools/v3/assert"
 )
 
 type MockSkupperCommand struct {
diff --git a/internal/cmd/skupper/connector/connector_test.go b/internal/cmd/skupper/connector/connector_test.go
index 432c0de7..556453e4 100644
--- a/internal/cmd/skupper/connector/connector_test.go
+++ b/internal/cmd/skupper/connector/connector_test.go
@@ -2,12 +2,13 @@ package connector
 
 import (
 	"fmt"
+	"testing"
+
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
-	"gotest.tools/assert"
-	"testing"
+	"gotest.tools/v3/assert"
 )
 
 func TestCmdConnectorFactory(t *testing.T) {
diff --git a/internal/cmd/skupper/connector/kube/connector_create_test.go b/internal/cmd/skupper/connector/kube/connector_create_test.go
index 42e8d2bd..7fd8f66f 100644
--- a/internal/cmd/skupper/connector/kube/connector_create_test.go
+++ b/internal/cmd/skupper/connector/kube/connector_create_test.go
@@ -9,7 +9,7 @@ import (
 
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	appsv1 "k8s.io/api/apps/v1"
 	v12 "k8s.io/api/core/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
diff --git a/internal/cmd/skupper/connector/kube/connector_delete_test.go b/internal/cmd/skupper/connector/kube/connector_delete_test.go
index a83445cb..e21ea838 100644
--- a/internal/cmd/skupper/connector/kube/connector_delete_test.go
+++ b/internal/cmd/skupper/connector/kube/connector_delete_test.go
@@ -9,7 +9,7 @@ import (
 
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 )
diff --git a/internal/cmd/skupper/connector/kube/connector_status_test.go b/internal/cmd/skupper/connector/kube/connector_status_test.go
index 4fc8e12d..ff4da117 100644
--- a/internal/cmd/skupper/connector/kube/connector_status_test.go
+++ b/internal/cmd/skupper/connector/kube/connector_status_test.go
@@ -1,13 +1,14 @@
 package kube
 
 import (
+	"testing"
+
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
-	"testing"
 
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 )
diff --git a/internal/cmd/skupper/connector/kube/connector_update_test.go b/internal/cmd/skupper/connector/kube/connector_update_test.go
index dc3c5eed..97984150 100644
--- a/internal/cmd/skupper/connector/kube/connector_update_test.go
+++ b/internal/cmd/skupper/connector/kube/connector_update_test.go
@@ -9,7 +9,7 @@ import (
 
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	appsv1 "k8s.io/api/apps/v1"
 	v12 "k8s.io/api/core/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
diff --git a/internal/cmd/skupper/connector/nonkube/connector_create_test.go b/internal/cmd/skupper/connector/nonkube/connector_create_test.go
index 228b8707..08472dd2 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_create_test.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_create_test.go
@@ -8,7 +8,7 @@ import (
 	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
 	"github.com/spf13/cobra"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	"k8s.io/apimachinery/pkg/runtime"
 )
 
diff --git a/internal/cmd/skupper/connector/nonkube/connector_delete_test.go b/internal/cmd/skupper/connector/nonkube/connector_delete_test.go
index 521ef67e..d83ebc8b 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_delete_test.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_delete_test.go
@@ -8,7 +8,7 @@ import (
 	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
 	"github.com/spf13/cobra"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	"k8s.io/apimachinery/pkg/runtime"
 )
 
diff --git a/internal/cmd/skupper/connector/nonkube/connector_status_test.go b/internal/cmd/skupper/connector/nonkube/connector_status_test.go
index 8608284b..a51e2d64 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_status_test.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_status_test.go
@@ -10,7 +10,7 @@ import (
 	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 	"github.com/skupperproject/skupper/pkg/nonkube/api"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 )
diff --git a/internal/cmd/skupper/connector/nonkube/connector_update_test.go b/internal/cmd/skupper/connector/nonkube/connector_update_test.go
index 3250969a..93f5619e 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_update_test.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_update_test.go
@@ -11,7 +11,7 @@ import (
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 	"github.com/skupperproject/skupper/pkg/nonkube/api"
 	"github.com/spf13/cobra"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 )
diff --git a/internal/cmd/skupper/link/kube/link_delete_test.go b/internal/cmd/skupper/link/kube/link_delete_test.go
index ba3ab580..029a243a 100644
--- a/internal/cmd/skupper/link/kube/link_delete_test.go
+++ b/internal/cmd/skupper/link/kube/link_delete_test.go
@@ -5,7 +5,7 @@ import (
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"testing"
diff --git a/internal/cmd/skupper/link/kube/link_generate_test.go b/internal/cmd/skupper/link/kube/link_generate_test.go
index a56f56bb..acba38cf 100644
--- a/internal/cmd/skupper/link/kube/link_generate_test.go
+++ b/internal/cmd/skupper/link/kube/link_generate_test.go
@@ -8,7 +8,7 @@ import (
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
diff --git a/internal/cmd/skupper/link/kube/link_status_test.go b/internal/cmd/skupper/link/kube/link_status_test.go
index 86678bfe..1fbd5b45 100644
--- a/internal/cmd/skupper/link/kube/link_status_test.go
+++ b/internal/cmd/skupper/link/kube/link_status_test.go
@@ -5,7 +5,7 @@ import (
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	"testing"
diff --git a/internal/cmd/skupper/link/kube/link_update_test.go b/internal/cmd/skupper/link/kube/link_update_test.go
index c5f83361..6717eaff 100644
--- a/internal/cmd/skupper/link/kube/link_update_test.go
+++ b/internal/cmd/skupper/link/kube/link_update_test.go
@@ -5,7 +5,7 @@ import (
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v12 "k8s.io/api/core/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
diff --git a/internal/cmd/skupper/link/link_test.go b/internal/cmd/skupper/link/link_test.go
index 6d0f21e0..5b92e516 100644
--- a/internal/cmd/skupper/link/link_test.go
+++ b/internal/cmd/skupper/link/link_test.go
@@ -6,7 +6,7 @@ import (
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	"testing"
 )
 
diff --git a/internal/cmd/skupper/listener/kube/listener_create_test.go b/internal/cmd/skupper/listener/kube/listener_create_test.go
index ed58e98c..8d4f5f69 100644
--- a/internal/cmd/skupper/listener/kube/listener_create_test.go
+++ b/internal/cmd/skupper/listener/kube/listener_create_test.go
@@ -9,7 +9,7 @@ import (
 
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v12 "k8s.io/api/core/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
diff --git a/internal/cmd/skupper/listener/kube/listener_delete_test.go b/internal/cmd/skupper/listener/kube/listener_delete_test.go
index 570f68f4..8849e705 100644
--- a/internal/cmd/skupper/listener/kube/listener_delete_test.go
+++ b/internal/cmd/skupper/listener/kube/listener_delete_test.go
@@ -9,7 +9,7 @@ import (
 
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 )
diff --git a/internal/cmd/skupper/listener/kube/listener_status_test.go b/internal/cmd/skupper/listener/kube/listener_status_test.go
index c4e51cbe..521a7047 100644
--- a/internal/cmd/skupper/listener/kube/listener_status_test.go
+++ b/internal/cmd/skupper/listener/kube/listener_status_test.go
@@ -7,7 +7,7 @@ import (
 
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 )
diff --git a/internal/cmd/skupper/listener/kube/listener_update_test.go b/internal/cmd/skupper/listener/kube/listener_update_test.go
index 5194ac1e..cbd3b4c1 100644
--- a/internal/cmd/skupper/listener/kube/listener_update_test.go
+++ b/internal/cmd/skupper/listener/kube/listener_update_test.go
@@ -9,7 +9,7 @@ import (
 
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v12 "k8s.io/api/core/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
diff --git a/internal/cmd/skupper/listener/listener_test.go b/internal/cmd/skupper/listener/listener_test.go
index eb7e3417..dbf0c0eb 100644
--- a/internal/cmd/skupper/listener/listener_test.go
+++ b/internal/cmd/skupper/listener/listener_test.go
@@ -6,7 +6,7 @@ import (
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	"testing"
 )
 
diff --git a/internal/cmd/skupper/site/kube/site_create_test.go b/internal/cmd/skupper/site/kube/site_create_test.go
index 761ef3be..2531ed8d 100644
--- a/internal/cmd/skupper/site/kube/site_create_test.go
+++ b/internal/cmd/skupper/site/kube/site_create_test.go
@@ -9,7 +9,7 @@ import (
 	"time"
 
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 )
diff --git a/internal/cmd/skupper/site/kube/site_delete_test.go b/internal/cmd/skupper/site/kube/site_delete_test.go
index 6e7b4c8f..76ad588b 100644
--- a/internal/cmd/skupper/site/kube/site_delete_test.go
+++ b/internal/cmd/skupper/site/kube/site_delete_test.go
@@ -9,7 +9,7 @@ import (
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 )
diff --git a/internal/cmd/skupper/site/kube/site_status_test.go b/internal/cmd/skupper/site/kube/site_status_test.go
index f642f999..601a530a 100644
--- a/internal/cmd/skupper/site/kube/site_status_test.go
+++ b/internal/cmd/skupper/site/kube/site_status_test.go
@@ -6,7 +6,7 @@ import (
 
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 )
diff --git a/internal/cmd/skupper/site/kube/site_update_test.go b/internal/cmd/skupper/site/kube/site_update_test.go
index 525032c9..a85c6dba 100644
--- a/internal/cmd/skupper/site/kube/site_update_test.go
+++ b/internal/cmd/skupper/site/kube/site_update_test.go
@@ -9,7 +9,7 @@ import (
 
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 )
diff --git a/internal/cmd/skupper/site/nonkube/site_create_test.go b/internal/cmd/skupper/site/nonkube/site_create_test.go
index a25f8da7..0d5ed353 100644
--- a/internal/cmd/skupper/site/nonkube/site_create_test.go
+++ b/internal/cmd/skupper/site/nonkube/site_create_test.go
@@ -7,7 +7,7 @@ import (
 	"github.com/spf13/cobra"
 	"testing"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	"k8s.io/apimachinery/pkg/runtime"
 )
 
diff --git a/internal/cmd/skupper/site/site_test.go b/internal/cmd/skupper/site/site_test.go
index f21ce86b..53339e1a 100644
--- a/internal/cmd/skupper/site/site_test.go
+++ b/internal/cmd/skupper/site/site_test.go
@@ -5,7 +5,7 @@ import (
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	"testing"
 )
 
diff --git a/internal/cmd/skupper/token/kube/token_issue_test.go b/internal/cmd/skupper/token/kube/token_issue_test.go
index 950711f1..7e17ec69 100644
--- a/internal/cmd/skupper/token/kube/token_issue_test.go
+++ b/internal/cmd/skupper/token/kube/token_issue_test.go
@@ -10,7 +10,7 @@ import (
 
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 )
diff --git a/internal/cmd/skupper/token/kube/token_redeem_test.go b/internal/cmd/skupper/token/kube/token_redeem_test.go
index d72a99ad..59de2fb1 100644
--- a/internal/cmd/skupper/token/kube/token_redeem_test.go
+++ b/internal/cmd/skupper/token/kube/token_redeem_test.go
@@ -12,7 +12,7 @@ import (
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 	"github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/scheme"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
diff --git a/internal/cmd/skupper/token/token_test.go b/internal/cmd/skupper/token/token_test.go
index 4a7f451e..c3608f6c 100644
--- a/internal/cmd/skupper/token/token_test.go
+++ b/internal/cmd/skupper/token/token_test.go
@@ -8,7 +8,7 @@ import (
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/spf13/cobra"
 	"github.com/spf13/pflag"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestCmdTokenFactory(t *testing.T) {
diff --git a/internal/flag/flag_test.go b/internal/flag/flag_test.go
index 6052cccb..e4811050 100644
--- a/internal/flag/flag_test.go
+++ b/internal/flag/flag_test.go
@@ -4,7 +4,7 @@ import (
 	"flag"
 	"testing"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func Test_StringVar(t *testing.T) {
diff --git a/internal/nonkube/bundle/common_test.go b/internal/nonkube/bundle/common_test.go
index fe2afe38..d0878bb7 100644
--- a/internal/nonkube/bundle/common_test.go
+++ b/internal/nonkube/bundle/common_test.go
@@ -3,7 +3,7 @@ package bundle
 import (
 	"testing"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestIsValidBundle(t *testing.T) {
diff --git a/internal/nonkube/bundle/extract_test.go b/internal/nonkube/bundle/extract_test.go
index 0d46ff71..29db11bc 100644
--- a/internal/nonkube/bundle/extract_test.go
+++ b/internal/nonkube/bundle/extract_test.go
@@ -11,7 +11,7 @@ import (
 	"github.com/skupperproject/skupper/internal/utils"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 	"github.com/skupperproject/skupper/pkg/nonkube/api"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
diff --git a/internal/nonkube/bundle/tarball_test.go b/internal/nonkube/bundle/tarball_test.go
index 83469697..1cdb7cdf 100644
--- a/internal/nonkube/bundle/tarball_test.go
+++ b/internal/nonkube/bundle/tarball_test.go
@@ -9,7 +9,7 @@ import (
 	"testing"
 
 	"github.com/skupperproject/skupper/internal/utils"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestTarballBundle_InstallFile(t *testing.T) {
diff --git a/internal/nonkube/client/compat/client_test.go b/internal/nonkube/client/compat/client_test.go
index 5e4a06c9..0028dca0 100644
--- a/internal/nonkube/client/compat/client_test.go
+++ b/internal/nonkube/client/compat/client_test.go
@@ -5,7 +5,7 @@ import (
 	"testing"
 
 	"github.com/skupperproject/skupper-libpod/v4/client/volumes_compat"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestToAPIError(t *testing.T) {
diff --git a/internal/nonkube/client/compat/container_test.go b/internal/nonkube/client/compat/container_test.go
index 438b5f68..a3cc2e0b 100644
--- a/internal/nonkube/client/compat/container_test.go
+++ b/internal/nonkube/client/compat/container_test.go
@@ -18,7 +18,7 @@ import (
 	"github.com/skupperproject/skupper/pkg/container"
 	"github.com/skupperproject/skupper/pkg/images"
 	"github.com/skupperproject/skupper/pkg/utils"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	"k8s.io/apimachinery/pkg/util/rand"
 )
 
diff --git a/internal/nonkube/client/compat/image_test.go b/internal/nonkube/client/compat/image_test.go
index 5bae957f..bb2be443 100644
--- a/internal/nonkube/client/compat/image_test.go
+++ b/internal/nonkube/client/compat/image_test.go
@@ -6,7 +6,7 @@ import (
 	"testing"
 
 	"github.com/skupperproject/skupper/pkg/container"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestImageMock(t *testing.T) {
diff --git a/internal/nonkube/client/compat/rest_test.go b/internal/nonkube/client/compat/rest_test.go
index f3662b01..73a51be4 100644
--- a/internal/nonkube/client/compat/rest_test.go
+++ b/internal/nonkube/client/compat/rest_test.go
@@ -14,7 +14,7 @@ import (
 	"time"
 
 	"github.com/skupperproject/skupper/pkg/utils"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 // TestNewPodmanClient if podman (binary) is available, it starts
diff --git a/internal/nonkube/client/compat/volume_test.go b/internal/nonkube/client/compat/volume_test.go
index f94241f9..2248f5ff 100644
--- a/internal/nonkube/client/compat/volume_test.go
+++ b/internal/nonkube/client/compat/volume_test.go
@@ -6,7 +6,7 @@ import (
 
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/pkg/container"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestVolume(t *testing.T) {
diff --git a/internal/utils/command_test.go b/internal/utils/command_test.go
index 8ceda65e..78ef1bd6 100644
--- a/internal/utils/command_test.go
+++ b/internal/utils/command_test.go
@@ -3,7 +3,7 @@ package utils
 import (
 	"testing"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 const (
diff --git a/internal/utils/files_test.go b/internal/utils/files_test.go
index b9c20c76..e25be98b 100644
--- a/internal/utils/files_test.go
+++ b/internal/utils/files_test.go
@@ -7,7 +7,7 @@ import (
 	"strings"
 	"testing"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestDirectoryReader(t *testing.T) {
diff --git a/internal/utils/tarball_test.go b/internal/utils/tarball_test.go
index 9372608a..f90f7201 100644
--- a/internal/utils/tarball_test.go
+++ b/internal/utils/tarball_test.go
@@ -9,7 +9,7 @@ import (
 	"testing"
 	"time"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestTarball(t *testing.T) {
diff --git a/pkg/fs/watcher_test.go b/pkg/fs/watcher_test.go
index 7785d8ed..c0f7250e 100644
--- a/pkg/fs/watcher_test.go
+++ b/pkg/fs/watcher_test.go
@@ -10,7 +10,7 @@ import (
 	"time"
 
 	"github.com/skupperproject/skupper/pkg/utils"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestFileWatcher(t *testing.T) {
diff --git a/pkg/kube/configmaps_test.go b/pkg/kube/configmaps_test.go
index 542b33e6..09d9aa07 100644
--- a/pkg/kube/configmaps_test.go
+++ b/pkg/kube/configmaps_test.go
@@ -6,7 +6,7 @@ import (
 	"reflect"
 	"testing"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
diff --git a/pkg/kube/flow/status_test.go b/pkg/kube/flow/status_test.go
index 2dca3fe0..86f020a5 100644
--- a/pkg/kube/flow/status_test.go
+++ b/pkg/kube/flow/status_test.go
@@ -15,7 +15,7 @@ import (
 	"github.com/skupperproject/skupper/pkg/vanflow/session"
 	"github.com/skupperproject/skupper/pkg/vanflow/store"
 	"golang.org/x/time/rate"
-	"gotest.tools/poll"
+	"gotest.tools/v3/poll"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes/fake"
diff --git a/pkg/kube/grants/autoconfigure_test.go b/pkg/kube/grants/autoconfigure_test.go
index 1999cfbc..13c53dfc 100644
--- a/pkg/kube/grants/autoconfigure_test.go
+++ b/pkg/kube/grants/autoconfigure_test.go
@@ -5,7 +5,7 @@ import (
 	"errors"
 	"testing"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 	k8stesting "k8s.io/client-go/testing"
diff --git a/pkg/kube/grants/config_test.go b/pkg/kube/grants/config_test.go
index e3891ff9..3e56dea5 100644
--- a/pkg/kube/grants/config_test.go
+++ b/pkg/kube/grants/config_test.go
@@ -5,7 +5,7 @@ import (
 	"os"
 	"testing"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func Test_BoundGrantConfig(t *testing.T) {
diff --git a/pkg/kube/grants/disabled_test.go b/pkg/kube/grants/disabled_test.go
index 148646ed..2716bf99 100644
--- a/pkg/kube/grants/disabled_test.go
+++ b/pkg/kube/grants/disabled_test.go
@@ -4,7 +4,7 @@ import (
 	"context"
 	"testing"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 
diff --git a/pkg/kube/grants/grant_test.go b/pkg/kube/grants/grant_test.go
index 5501be20..3c0ae3b0 100644
--- a/pkg/kube/grants/grant_test.go
+++ b/pkg/kube/grants/grant_test.go
@@ -10,7 +10,7 @@ import (
 	"testing"
 	"time"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	meta "k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
diff --git a/pkg/kube/grants/init_test.go b/pkg/kube/grants/init_test.go
index a092337b..eaca2fbe 100644
--- a/pkg/kube/grants/init_test.go
+++ b/pkg/kube/grants/init_test.go
@@ -4,7 +4,7 @@ import (
 	"context"
 	"testing"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
 
diff --git a/pkg/kube/grants/redeem_test.go b/pkg/kube/grants/redeem_test.go
index 8822523c..8ed0afd5 100644
--- a/pkg/kube/grants/redeem_test.go
+++ b/pkg/kube/grants/redeem_test.go
@@ -10,7 +10,7 @@ import (
 	"net/http"
 	"testing"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	meta "k8s.io/apimachinery/pkg/api/meta"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/runtime"
diff --git a/pkg/kube/grants/server_test.go b/pkg/kube/grants/server_test.go
index 5181e011..f09d9a39 100644
--- a/pkg/kube/grants/server_test.go
+++ b/pkg/kube/grants/server_test.go
@@ -6,7 +6,7 @@ import (
 	"net/http"
 	"testing"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	corev1 "k8s.io/api/core/v1"
 )
 
diff --git a/pkg/kube/grants/tokens_test.go b/pkg/kube/grants/tokens_test.go
index d1a5a019..31fd2399 100644
--- a/pkg/kube/grants/tokens_test.go
+++ b/pkg/kube/grants/tokens_test.go
@@ -5,7 +5,7 @@ import (
 	"io"
 	"testing"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 )
diff --git a/pkg/kube/securedaccess/access_test.go b/pkg/kube/securedaccess/access_test.go
index 35cc9357..7d4674f3 100644
--- a/pkg/kube/securedaccess/access_test.go
+++ b/pkg/kube/securedaccess/access_test.go
@@ -13,8 +13,8 @@ import (
 	fakeclient "github.com/skupperproject/skupper/internal/kube/client/fake"
 	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 	"github.com/skupperproject/skupper/pkg/kube"
-	"gotest.tools/assert"
-	"gotest.tools/assert/cmp"
+	"gotest.tools/v3/assert"
+	"gotest.tools/v3/assert/cmp"
 	corev1 "k8s.io/api/core/v1"
 	networkingv1 "k8s.io/api/networking/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
diff --git a/pkg/kube/securedaccess/config_test.go b/pkg/kube/securedaccess/config_test.go
index a703d6ba..4b7f3379 100644
--- a/pkg/kube/securedaccess/config_test.go
+++ b/pkg/kube/securedaccess/config_test.go
@@ -4,7 +4,7 @@ import (
 	"flag"
 	"testing"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func Test_BoundConfig(t *testing.T) {
diff --git a/pkg/kube/securedaccess/contour_test.go b/pkg/kube/securedaccess/contour_test.go
index 8104f48c..9a4e64c8 100644
--- a/pkg/kube/securedaccess/contour_test.go
+++ b/pkg/kube/securedaccess/contour_test.go
@@ -3,7 +3,7 @@ package securedaccess
 import (
 	"testing"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
 )
 
diff --git a/pkg/kube/securedaccess/utils_test.go b/pkg/kube/securedaccess/utils_test.go
index 915b3bda..cdb5bda4 100644
--- a/pkg/kube/securedaccess/utils_test.go
+++ b/pkg/kube/securedaccess/utils_test.go
@@ -3,7 +3,7 @@ package securedaccess
 import (
 	"testing"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func Test_possibleKeyPortNamePairs(t *testing.T) {
diff --git a/pkg/kube/securedaccess/watchers_test.go b/pkg/kube/securedaccess/watchers_test.go
index 0a6c4bfe..376d5714 100644
--- a/pkg/kube/securedaccess/watchers_test.go
+++ b/pkg/kube/securedaccess/watchers_test.go
@@ -13,8 +13,8 @@ import (
 	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 	fakev2alpha1 "github.com/skupperproject/skupper/pkg/generated/client/clientset/versioned/typed/skupper/v2alpha1/fake"
 	"github.com/skupperproject/skupper/pkg/kube"
-	"gotest.tools/assert"
-	"gotest.tools/assert/cmp"
+	"gotest.tools/v3/assert"
+	"gotest.tools/v3/assert/cmp"
 	corev1 "k8s.io/api/core/v1"
 	networkingv1 "k8s.io/api/networking/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
diff --git a/pkg/kube/site/bindings_test.go b/pkg/kube/site/bindings_test.go
index 67c16061..1fb1cca8 100644
--- a/pkg/kube/site/bindings_test.go
+++ b/pkg/kube/site/bindings_test.go
@@ -5,7 +5,7 @@ import (
 
 	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 	"github.com/skupperproject/skupper/pkg/qdr"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
@@ -29,13 +29,15 @@ func (m *MockBindingContext) Select(connector *skupperv2alpha1.Connector) Target
 	return nil
 }
 
-func (m *MockBindingContext) Expose(ports *ExposedPortSet) {
+func (m *MockBindingContext) Expose(ports *ExposedPortSet) error {
 	portsCopy := *ports
 	m.exposed[ports.Host] = &portsCopy
+	return nil
 }
 
-func (m *MockBindingContext) Unexpose(host string) {
+func (m *MockBindingContext) Unexpose(host string) error {
 	m.unexposedHost = host
+	return nil
 }
 
 type MockTargetSelection struct {
@@ -165,7 +167,7 @@ func TestBindingAdaptor_ConnectorUpdated(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			a := &BindingAdaptor{
+			a := &ExtendedBindings{
 				context:   tt.fields.context,
 				mapping:   tt.fields.mapping,
 				exposed:   tt.fields.exposed,
@@ -223,7 +225,7 @@ func TestBindingAdaptor_ConnectorDeleted(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			a := &BindingAdaptor{
+			a := &ExtendedBindings{
 				context:   tt.fields.context,
 				mapping:   tt.fields.mapping,
 				exposed:   tt.fields.exposed,
@@ -389,7 +391,7 @@ func TestBindingAdaptor_updateBridgeConfigForConnector(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			a := &BindingAdaptor{
+			a := &ExtendedBindings{
 				context:   tt.fields.context,
 				mapping:   tt.fields.mapping,
 				exposed:   tt.fields.exposed,
@@ -464,7 +466,7 @@ func TestBindingAdaptor_ListenerUpdated(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			a := &BindingAdaptor{
+			a := &ExtendedBindings{
 				context:   tt.fields.context,
 				mapping:   tt.fields.mapping,
 				exposed:   tt.fields.exposed,
@@ -526,12 +528,13 @@ func TestBindingAdaptor_ListenerDeleted(t *testing.T) {
 
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			a := &BindingAdaptor{
+			a := &ExtendedBindings{
 				context:   tt.fields.context,
 				mapping:   tt.fields.mapping,
 				exposed:   tt.fields.exposed,
 				selectors: tt.fields.selectors,
 			}
+			a.ListenerUpdated(tt.args.listener)
 			a.ListenerDeleted(tt.args.listener)
 
 			mockBindingContext, ok := a.context.(*MockBindingContext)
@@ -604,7 +607,7 @@ func TestBindingAdaptor_updateBridgeConfigForListener(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			a := &BindingAdaptor{
+			a := &ExtendedBindings{
 				context:   tt.fields.context,
 				mapping:   tt.fields.mapping,
 				exposed:   tt.fields.exposed,
diff --git a/pkg/kube/site/site_test.go b/pkg/kube/site/site_test.go
index 80d2090a..410c1695 100644
--- a/pkg/kube/site/site_test.go
+++ b/pkg/kube/site/site_test.go
@@ -13,7 +13,7 @@ import (
 	"github.com/skupperproject/skupper/pkg/qdr"
 	site1 "github.com/skupperproject/skupper/pkg/site"
 	"github.com/skupperproject/skupper/pkg/version"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -1038,12 +1038,12 @@ func newSiteMocks(namespace string, k8sObjects []runtime.Object, skupperObjects
 		linkAccess: make(map[string]*skupperv2alpha1.RouterAccess),
 		certs:      certificates.NewCertificateManager(controller),
 		access:     securedaccess.NewSecuredAccessManager(client, nil, &securedaccess.Config{DefaultAccessType: "loadbalancer"}, securedaccess.ControllerContext{}),
-		adaptor:    BindingAdaptor{},
 		routerPods: make(map[string]*corev1.Pod),
 		logger: slog.New(slog.Default().Handler()).With(
 			slog.String("component", "kube.site.site"),
 		),
 	}
+	newSite.bindings.init(NewMockBindingContext(map[string]TargetSelection{}), &qdr.RouterConfig{})
 
 	newSite.site = site
 	newSite.name = site.ObjectMeta.Name
diff --git a/pkg/network/network_test.go b/pkg/network/network_test.go
index 788177ce..ce9d7ddf 100644
--- a/pkg/network/network_test.go
+++ b/pkg/network/network_test.go
@@ -2,7 +2,7 @@ package network
 
 import (
 	"encoding/json"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	"testing"
 )
 
diff --git a/pkg/nonkube/api/environment_test.go b/pkg/nonkube/api/environment_test.go
index 5e5909e3..e77cfb70 100644
--- a/pkg/nonkube/api/environment_test.go
+++ b/pkg/nonkube/api/environment_test.go
@@ -12,7 +12,7 @@ import (
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 	"github.com/skupperproject/skupper/pkg/config"
 	"gopkg.in/yaml.v3"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
diff --git a/pkg/nonkube/api/site_state_test.go b/pkg/nonkube/api/site_state_test.go
index 9c194af5..e11f943f 100644
--- a/pkg/nonkube/api/site_state_test.go
+++ b/pkg/nonkube/api/site_state_test.go
@@ -7,7 +7,7 @@ import (
 	"testing"
 
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
diff --git a/pkg/nonkube/api/token_test.go b/pkg/nonkube/api/token_test.go
index 4b607dc9..fa371f5e 100644
--- a/pkg/nonkube/api/token_test.go
+++ b/pkg/nonkube/api/token_test.go
@@ -10,7 +10,7 @@ import (
 
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 	"github.com/skupperproject/skupper/pkg/certs"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
diff --git a/pkg/nonkube/bundle/container_test.go b/pkg/nonkube/bundle/container_test.go
index a9a41cae..79331511 100644
--- a/pkg/nonkube/bundle/container_test.go
+++ b/pkg/nonkube/bundle/container_test.go
@@ -5,7 +5,7 @@ import (
 	"testing"
 
 	"github.com/skupperproject/skupper/pkg/container"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestEscapeCommand(t *testing.T) {
diff --git a/pkg/nonkube/common/fs_config_renderer_test.go b/pkg/nonkube/common/fs_config_renderer_test.go
index e34d6c77..62ab7906 100644
--- a/pkg/nonkube/common/fs_config_renderer_test.go
+++ b/pkg/nonkube/common/fs_config_renderer_test.go
@@ -10,7 +10,7 @@ import (
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 	"github.com/skupperproject/skupper/pkg/certs"
 	"github.com/skupperproject/skupper/pkg/nonkube/api"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
diff --git a/pkg/nonkube/common/site_state_loader_test.go b/pkg/nonkube/common/site_state_loader_test.go
index a9a28de3..0ee02c53 100644
--- a/pkg/nonkube/common/site_state_loader_test.go
+++ b/pkg/nonkube/common/site_state_loader_test.go
@@ -6,7 +6,7 @@ import (
 	"testing"
 
 	"github.com/skupperproject/skupper/pkg/nonkube/api"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestFileSystemSiteStateLoder(t *testing.T) {
diff --git a/pkg/nonkube/common/site_state_renderer_common_test.go b/pkg/nonkube/common/site_state_renderer_common_test.go
index b1deaa50..71d48048 100644
--- a/pkg/nonkube/common/site_state_renderer_common_test.go
+++ b/pkg/nonkube/common/site_state_renderer_common_test.go
@@ -4,7 +4,7 @@ import (
 	"reflect"
 	"testing"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestCopySiteState(t *testing.T) {
diff --git a/pkg/nonkube/common/site_state_validator_test.go b/pkg/nonkube/common/site_state_validator_test.go
index bec91013..edff96a3 100644
--- a/pkg/nonkube/common/site_state_validator_test.go
+++ b/pkg/nonkube/common/site_state_validator_test.go
@@ -5,7 +5,7 @@ import (
 
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 	"github.com/skupperproject/skupper/pkg/nonkube/api"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
diff --git a/pkg/nonkube/common/startup_test.go b/pkg/nonkube/common/startup_test.go
index 4b83e4cb..a88d42d9 100644
--- a/pkg/nonkube/common/startup_test.go
+++ b/pkg/nonkube/common/startup_test.go
@@ -9,7 +9,7 @@ import (
 
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/pkg/nonkube/api"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestGetStartupScripts(t *testing.T) {
diff --git a/pkg/nonkube/common/systemd_test.go b/pkg/nonkube/common/systemd_test.go
index 8252aac0..91155dd8 100644
--- a/pkg/nonkube/common/systemd_test.go
+++ b/pkg/nonkube/common/systemd_test.go
@@ -10,7 +10,7 @@ import (
 
 	"github.com/skupperproject/skupper/pkg/nonkube/api"
 	"github.com/skupperproject/skupper/pkg/utils"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestSystemdService(t *testing.T) {
diff --git a/pkg/qdr/amqp_mgmt_mock_test.go b/pkg/qdr/amqp_mgmt_mock_test.go
index bc186660..c9c8dd0a 100644
--- a/pkg/qdr/amqp_mgmt_mock_test.go
+++ b/pkg/qdr/amqp_mgmt_mock_test.go
@@ -7,7 +7,7 @@ import (
 
 	"encoding/json"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 var clusterRun = flag.Bool("use-cluster", false, "run tests against a configured cluster")
diff --git a/pkg/qdr/qdr_test.go b/pkg/qdr/qdr_test.go
index d233b7ba..801b50c3 100644
--- a/pkg/qdr/qdr_test.go
+++ b/pkg/qdr/qdr_test.go
@@ -7,7 +7,7 @@ import (
 
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/pkg/utils"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
diff --git a/pkg/site/bindings_test.go b/pkg/site/bindings_test.go
index 5e8476fa..846aed3e 100644
--- a/pkg/site/bindings_test.go
+++ b/pkg/site/bindings_test.go
@@ -5,7 +5,7 @@ import (
 
 	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 	"github.com/skupperproject/skupper/pkg/qdr"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
@@ -546,7 +546,7 @@ func getListenerHostPortOverride(host string, port int) ListenerConfiguration {
 func getPodConnectorConfiguration(pods []skupperv2alpha1.PodDetails) ConnectorConfiguration {
 	return func(siteId string, connector *skupperv2alpha1.Connector, config *qdr.BridgeConfig) {
 		for _, pod := range pods {
-			UpdateBridgeConfigForConnectorToPod(siteId, connector, pod, config)
+			UpdateBridgeConfigForConnectorToPod(siteId, connector, pod, false, config)
 		}
 	}
 }
diff --git a/pkg/site/connector_test.go b/pkg/site/connector_test.go
index e3c9c860..afa9000a 100644
--- a/pkg/site/connector_test.go
+++ b/pkg/site/connector_test.go
@@ -5,7 +5,7 @@ import (
 
 	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 	"github.com/skupperproject/skupper/pkg/qdr"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
diff --git a/pkg/site/link_test.go b/pkg/site/link_test.go
index f2e2ed79..ecf0925c 100644
--- a/pkg/site/link_test.go
+++ b/pkg/site/link_test.go
@@ -6,7 +6,7 @@ import (
 
 	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 	"github.com/skupperproject/skupper/pkg/qdr"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
diff --git a/pkg/site/listener_test.go b/pkg/site/listener_test.go
index b2c8ec73..e93aa872 100644
--- a/pkg/site/listener_test.go
+++ b/pkg/site/listener_test.go
@@ -5,7 +5,7 @@ import (
 
 	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 	"github.com/skupperproject/skupper/pkg/qdr"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 
 	//	v1 "k8s.io/client-go/applyconfigurations/meta/v1"
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
diff --git a/pkg/utils/configs/manifest_test.go b/pkg/utils/configs/manifest_test.go
index 5893fd25..858b72ec 100644
--- a/pkg/utils/configs/manifest_test.go
+++ b/pkg/utils/configs/manifest_test.go
@@ -3,7 +3,7 @@ package configs
 import (
 	"fmt"
 	"github.com/skupperproject/skupper/pkg/images"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	"os"
 	"strings"
 	"testing"
diff --git a/pkg/utils/tcp_test.go b/pkg/utils/tcp_test.go
index b256abc4..2fb0db87 100644
--- a/pkg/utils/tcp_test.go
+++ b/pkg/utils/tcp_test.go
@@ -7,7 +7,7 @@ import (
 	"sync"
 	"testing"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestTcpPortNextFree(t *testing.T) {
diff --git a/pkg/utils/utils_test.go b/pkg/utils/utils_test.go
index 4bd4746d..610af60f 100644
--- a/pkg/utils/utils_test.go
+++ b/pkg/utils/utils_test.go
@@ -1,7 +1,7 @@
 package utils
 
 import (
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 	"reflect"
 	"testing"
 )
diff --git a/pkg/utils/validator/simple_validator_test.go b/pkg/utils/validator/simple_validator_test.go
index 89cde020..7d14a843 100644
--- a/pkg/utils/validator/simple_validator_test.go
+++ b/pkg/utils/validator/simple_validator_test.go
@@ -6,7 +6,7 @@ import (
 	"testing"
 	"time"
 
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestNewStringValidator(t *testing.T) {
diff --git a/pkg/vanflow/base_test.go b/pkg/vanflow/base_test.go
index 96ad3fa1..12c14967 100644
--- a/pkg/vanflow/base_test.go
+++ b/pkg/vanflow/base_test.go
@@ -5,7 +5,7 @@ import (
 	"time"
 
 	"github.com/skupperproject/skupper/pkg/vanflow/encoding"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestTimeEncoding(t *testing.T) {
diff --git a/pkg/vanflow/encoding/encoder_test.go b/pkg/vanflow/encoding/encoder_test.go
index 078e7c68..9f242bab 100644
--- a/pkg/vanflow/encoding/encoder_test.go
+++ b/pkg/vanflow/encoding/encoder_test.go
@@ -7,7 +7,7 @@ import (
 
 	"github.com/skupperproject/skupper/pkg/vanflow"
 	"github.com/skupperproject/skupper/pkg/vanflow/encoding"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 type tRecordAttributeEncodeDecode struct {
diff --git a/pkg/vanflow/eventsource/client_test.go b/pkg/vanflow/eventsource/client_test.go
index 739acb06..d6e2a8c9 100644
--- a/pkg/vanflow/eventsource/client_test.go
+++ b/pkg/vanflow/eventsource/client_test.go
@@ -15,7 +15,7 @@ import (
 	amqp "github.com/Azure/go-amqp"
 	"github.com/skupperproject/skupper/pkg/vanflow"
 	"github.com/skupperproject/skupper/pkg/vanflow/session"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestClient(t *testing.T) {
diff --git a/pkg/vanflow/eventsource/discovery_test.go b/pkg/vanflow/eventsource/discovery_test.go
index 0015c7f8..fc12042d 100644
--- a/pkg/vanflow/eventsource/discovery_test.go
+++ b/pkg/vanflow/eventsource/discovery_test.go
@@ -9,8 +9,8 @@ import (
 	"github.com/google/go-cmp/cmp/cmpopts"
 	"github.com/skupperproject/skupper/pkg/vanflow"
 	"github.com/skupperproject/skupper/pkg/vanflow/session"
-	"gotest.tools/assert"
-	"gotest.tools/poll"
+	"gotest.tools/v3/assert"
+	"gotest.tools/v3/poll"
 )
 
 func TestDiscoveryBasic(t *testing.T) {
diff --git a/pkg/vanflow/eventsource/manager_test.go b/pkg/vanflow/eventsource/manager_test.go
index 7796b3a1..805e1964 100644
--- a/pkg/vanflow/eventsource/manager_test.go
+++ b/pkg/vanflow/eventsource/manager_test.go
@@ -10,7 +10,7 @@ import (
 	"github.com/google/go-cmp/cmp/cmpopts"
 	"github.com/skupperproject/skupper/pkg/vanflow"
 	"github.com/skupperproject/skupper/pkg/vanflow/store"
-	"gotest.tools/poll"
+	"gotest.tools/v3/poll"
 )
 
 func TestManagerClient(t *testing.T) {
diff --git a/pkg/vanflow/messages_test.go b/pkg/vanflow/messages_test.go
index dc4f7b6f..d9f9b8da 100644
--- a/pkg/vanflow/messages_test.go
+++ b/pkg/vanflow/messages_test.go
@@ -5,7 +5,7 @@ import (
 	"time"
 
 	amqp "github.com/Azure/go-amqp"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestDecode(t *testing.T) {
diff --git a/pkg/vanflow/session/container_test.go b/pkg/vanflow/session/container_test.go
index c96449c4..b904795b 100644
--- a/pkg/vanflow/session/container_test.go
+++ b/pkg/vanflow/session/container_test.go
@@ -8,7 +8,7 @@ import (
 	"time"
 
 	"github.com/Azure/go-amqp"
-	"gotest.tools/assert"
+	"gotest.tools/v3/assert"
 )
 
 func TestContainerPing(t *testing.T) {