Repository: skupperproject/skupper
Pull request #1791 (2024-11-18 17:11:46)

GetPlatform enhancement to the CLI
Moving GetPlatform function to internal pkg and including os.Args to be evaluated.
This helps the CLI properly getting the platform when `--platform` flag is provided.

nluaces: I think this is not necessary to do here, I can fix it in the commands by getting the value of the flag.
nluaces: when configuring a command, the default is the value of GetPlatform, unless the platform flag was specified:
https://github.com/skupperproject/skupper/blob/781927cf1dd432e5c839a5d0b5ed2e9efd597a74/internal/cmd/skupper/common/command.go#L37
fgiorgetti: > when configuring a command, the default is the value of GetPlatform, unless the platform flag was specified:
> 
> https://github.com/skupperproject/skupper/blob/781927cf1dd432e5c839a5d0b5ed2e9efd597a74/internal/cmd/skupper/common/command.go#L37

For flag validation, I think this is working.

But for the generic commands being added, like in the code below:

https://github.com/skupperproject/skupper/blob/781927cf1dd432e5c839a5d0b5ed2e9efd597a74/internal/cmd/skupper/site/site.go#L27-L30

The platform is coming straight from `config.GetPlatform` which is causing incorrect flags to show up as part
of the `--help` message when using non-kube platforms.
nluaces: CmdSiteCreateFactory is calling ConfigureCobraCommand that checks the value of the flag

https://github.com/skupperproject/skupper/blob/781927cf1dd432e5c839a5d0b5ed2e9efd597a74/internal/cmd/skupper/site/site.go#L47C15-L47C38
fgiorgetti: > CmdSiteCreateFactory is calling ConfigureCobraCommand that checks the value of the flag
> 
> https://github.com/skupperproject/skupper/blob/781927cf1dd432e5c839a5d0b5ed2e9efd597a74/internal/cmd/skupper/site/site.go#L47C15-L47C38
> 
> All the factories are calling ConfigureCobracommand that checks the platform flag.

Correct.

But when `CmdSiteCreateFactory()`  is being called the `configuredPlatform` variable is set using `config.GetPlatform()`.
So it is not yet including the value of `--platform` flag.

As an example, looking at the #1778 PR and building the skupper cli, then showing `site create --help`, I do not 
see the non-kube flags (bind-host and subject-alternative-names):

```
$ make build-cmd 
GOOS=linux GOARCH=amd64 go build -ldflags="-s -w  -X github.com/skupperproject/skupper/pkg/version.Version=2.0.0-preview-1-46-g0ef2360d-modified"  -o skupper ./cmd/skupper
$ ./skupper --platform podman help site create
A site is a place where components of your application are running.
Sites are linked to form application networks.
There can be only one site definition per namespace.

Usage:
  skupper site create <name> [flags]

Flags:
      --enable-link-access        allow access for incoming links from remote sites (default: false)
  -h, --help                      help for create
      --link-access-type string   configure external access for links from remote sites.
                                  Choices: [route|loadbalancer]. Default: On OpenShift, route is the default; 
                                  for other Kubernetes flavors, loadbalancer is the default.
  -o, --output string             print resources to the console instead of submitting them to the Skupper controller. Choices: json, yaml
      --service-account string    the Kubernetes service account under which to run the Skupper controller
      --timeout duration          raise an error if the operation does not complete in the given period of time (expressed in seconds). (default 1m0s)

Global Flags:
  -c, --context string      Set the kubeconfig context
      --kubeconfig string   Path to the kubeconfig file to use
  -n, --namespace string    Set the namespace
  -p, --platform string     Set the platform type to use [kubernetes, podman, docker, systemd]
```

fgiorgetti: Maybe instead of passing `config.GetPlatform()` when adding commands, we should call a method that does both
things: parsing the `--platform` flag, then calling `config.GetPlatform()` next, leaving the latter as is. Thoughts?
nluaces: Now I understand your scenario, using --help flag in conjunction with --p flag. The cobra help function was not overridden, so I'm hiding commands not in execution but when defining and configuring the command, letting the --help flag not use the platform passed with the -p flag.

Let me check how to override the help function first.
nluaces: Using the value of the -p flag while executing --help would imply override all the help from all the commands. Which is not a good solution in comparison with this one. My bad!

(One of the main points of ConfigureCobraCommand was precisely to avoid parsing the -p flag as a string argument and instead use the cobra methods.)



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/bootstrap/bootstrap.go b/cmd/bootstrap/bootstrap.go
index a0b2b803..14cedf44 100644
--- a/cmd/bootstrap/bootstrap.go
+++ b/cmd/bootstrap/bootstrap.go
@@ -9,9 +9,9 @@ import (
 	"path/filepath"
 
 	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/internal/config"
 	internalbundle "github.com/skupperproject/skupper/internal/nonkube/bundle"
 	internalutils "github.com/skupperproject/skupper/internal/utils"
-	"github.com/skupperproject/skupper/pkg/config"
 	"github.com/skupperproject/skupper/pkg/nonkube/api"
 	"github.com/skupperproject/skupper/pkg/nonkube/bundle"
 	"github.com/skupperproject/skupper/pkg/nonkube/common"
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/internal/cmd/skupper/connector/connector.go b/internal/cmd/skupper/connector/connector.go
index 900d276c..fa30d6d2 100644
--- a/internal/cmd/skupper/connector/connector.go
+++ b/internal/cmd/skupper/connector/connector.go
@@ -1,13 +1,14 @@
 package connector
 
 import (
+	"time"
+
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/connector/kube"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/connector/nonkube"
-	"github.com/skupperproject/skupper/pkg/config"
+	"github.com/skupperproject/skupper/internal/config"
 	"github.com/spf13/cobra"
-	"time"
 )
 
 func NewCmdConnector() *cobra.Command {
diff --git a/internal/cmd/skupper/debug/debug.go b/internal/cmd/skupper/debug/debug.go
index c5273690..a107e772 100644
--- a/internal/cmd/skupper/debug/debug.go
+++ b/internal/cmd/skupper/debug/debug.go
@@ -5,8 +5,8 @@ import (
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/debug/kube"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/debug/nonkube"
+	"github.com/skupperproject/skupper/internal/config"
 
-	"github.com/skupperproject/skupper/pkg/config"
 	"github.com/spf13/cobra"
 )
 
diff --git a/internal/cmd/skupper/link/link.go b/internal/cmd/skupper/link/link.go
index 38176bb7..5b698067 100644
--- a/internal/cmd/skupper/link/link.go
+++ b/internal/cmd/skupper/link/link.go
@@ -1,13 +1,14 @@
 package link
 
 import (
+	"time"
+
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/link/kube"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/link/nonkube"
-	"github.com/skupperproject/skupper/pkg/config"
+	"github.com/skupperproject/skupper/internal/config"
 	"github.com/spf13/cobra"
-	"time"
 )
 
 func NewCmdLink() *cobra.Command {
diff --git a/internal/cmd/skupper/listener/listener.go b/internal/cmd/skupper/listener/listener.go
index bef9aa7c..6675ee60 100644
--- a/internal/cmd/skupper/listener/listener.go
+++ b/internal/cmd/skupper/listener/listener.go
@@ -1,13 +1,14 @@
 package listener
 
 import (
+	"time"
+
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/listener/kube"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/listener/nonkube"
-	"github.com/skupperproject/skupper/pkg/config"
+	"github.com/skupperproject/skupper/internal/config"
 	"github.com/spf13/cobra"
-	"time"
 )
 
 func NewCmdListener() *cobra.Command {
diff --git a/internal/cmd/skupper/root/root.go b/internal/cmd/skupper/root/root.go
index add52cda..098efb22 100644
--- a/internal/cmd/skupper/root/root.go
+++ b/internal/cmd/skupper/root/root.go
@@ -10,7 +10,7 @@ import (
 	"github.com/skupperproject/skupper/internal/cmd/skupper/site"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/token"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/version"
-	"github.com/skupperproject/skupper/pkg/config"
+	"github.com/skupperproject/skupper/internal/config"
 	"github.com/spf13/cobra"
 )
 
diff --git a/internal/cmd/skupper/site/site.go b/internal/cmd/skupper/site/site.go
index 35df6077..8b5c2549 100644
--- a/internal/cmd/skupper/site/site.go
+++ b/internal/cmd/skupper/site/site.go
@@ -10,7 +10,7 @@ import (
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/site/kube"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/site/nonkube"
-	"github.com/skupperproject/skupper/pkg/config"
+	"github.com/skupperproject/skupper/internal/config"
 	"github.com/spf13/cobra"
 )
 
@@ -25,9 +25,9 @@ skupper site status`,
 	}
 
 	cmd.AddCommand(CmdSiteCreateFactory(config.GetPlatform()))
-	cmd.AddCommand(CmdSiteUpdateFactory(config.GetPlatform()))
 	cmd.AddCommand(CmdSiteStatusFactory(config.GetPlatform()))
 	cmd.AddCommand(CmdSiteDeleteFactory(config.GetPlatform()))
+	cmd.AddCommand(CmdSiteUpdateFactory(config.GetPlatform()))
 
 	return cmd
 }
diff --git a/internal/cmd/skupper/token/token.go b/internal/cmd/skupper/token/token.go
index ca3ec60e..274c3dc3 100644
--- a/internal/cmd/skupper/token/token.go
+++ b/internal/cmd/skupper/token/token.go
@@ -7,8 +7,8 @@ import (
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/token/kube"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/token/nonkube"
+	"github.com/skupperproject/skupper/internal/config"
 
-	"github.com/skupperproject/skupper/pkg/config"
 	"github.com/spf13/cobra"
 )
 
diff --git a/internal/cmd/skupper/version/version.go b/internal/cmd/skupper/version/version.go
index b99b5d70..44a5e343 100644
--- a/internal/cmd/skupper/version/version.go
+++ b/internal/cmd/skupper/version/version.go
@@ -4,10 +4,10 @@ import (
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common"
 	"github.com/skupperproject/skupper/internal/cmd/skupper/version/nonkube"
+	"github.com/skupperproject/skupper/internal/config"
 
 	"github.com/skupperproject/skupper/internal/cmd/skupper/version/kube"
 
-	"github.com/skupperproject/skupper/pkg/config"
 	"github.com/spf13/cobra"
 )
 
diff --git a/pkg/config/platform.go b/internal/config/platform.go
similarity index 87%
rename from pkg/config/platform.go
rename to internal/config/platform.go
index cf10f683..aa005015 100644
--- a/pkg/config/platform.go
+++ b/internal/config/platform.go
@@ -7,6 +7,8 @@ import (
 	"os"
 	"path"
 	"path/filepath"
+	"slices"
+	"strings"
 
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/pkg/utils"
@@ -142,11 +144,25 @@ var (
 // will be returned.
 func GetPlatform() types.Platform {
 	p := &PlatformInfo{}
+	var platform types.Platform
 	_ = p.Load()
-	platform := types.Platform(utils.DefaultStr(Platform,
-		os.Getenv(types.ENV_PLATFORM),
-		string(p.Current),
-		string(types.PlatformKubernetes)))
+	for i, arg := range os.Args {
+		if slices.Contains([]string{"--platform", "-p"}, arg) && i+1 < len(os.Args) {
+			platformArg := os.Args[i+1]
+			platform = types.Platform(platformArg)
+			break
+		} else if strings.HasPrefix(arg, "--platform=") || strings.HasPrefix(arg, "-p=") {
+			platformArg := strings.Split(arg, "=")[1]
+			platform = types.Platform(platformArg)
+			break
+		}
+	}
+	if platform == "" {
+		platform = types.Platform(utils.DefaultStr(Platform,
+			os.Getenv(types.ENV_PLATFORM),
+			string(p.Current),
+			string(types.PlatformKubernetes)))
+	}
 	switch platform {
 	case types.PlatformPodman:
 		return types.PlatformPodman
diff --git a/internal/kube/adaptor/collector.go b/internal/kube/adaptor/collector.go
index 842fa9d1..4db586e3 100644
--- a/internal/kube/adaptor/collector.go
+++ b/internal/kube/adaptor/collector.go
@@ -9,8 +9,8 @@ import (
 	"time"
 
 	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/internal/config"
 	internalclient "github.com/skupperproject/skupper/internal/kube/client"
-	"github.com/skupperproject/skupper/pkg/config"
 	"github.com/skupperproject/skupper/pkg/kube"
 	kubeflow "github.com/skupperproject/skupper/pkg/kube/flow"
 	"github.com/skupperproject/skupper/pkg/vanflow"
diff --git a/internal/nonkube/client/compat/container_mock.go b/internal/nonkube/client/compat/container_mock.go
index 931c3206..f0c31385 100644
--- a/internal/nonkube/client/compat/container_mock.go
+++ b/internal/nonkube/client/compat/container_mock.go
@@ -19,7 +19,7 @@ import (
 	"github.com/skupperproject/skupper-libpod/v4/client/volumes_compat"
 	"github.com/skupperproject/skupper-libpod/v4/models"
 	"github.com/skupperproject/skupper/api/types"
-	"github.com/skupperproject/skupper/pkg/config"
+	"github.com/skupperproject/skupper/internal/config"
 	"github.com/skupperproject/skupper/pkg/container"
 )
 
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/qdr/amqp_mgmt.go b/pkg/qdr/amqp_mgmt.go
index 4811d9f2..61a8f95d 100644
--- a/pkg/qdr/amqp_mgmt.go
+++ b/pkg/qdr/amqp_mgmt.go
@@ -12,7 +12,7 @@ import (
 
 	amqp "github.com/interconnectedcloud/go-amqp"
 	"github.com/skupperproject/skupper/api/types"
-	"github.com/skupperproject/skupper/pkg/config"
+	"github.com/skupperproject/skupper/internal/config"
 	"github.com/skupperproject/skupper/pkg/utils"
 )
 
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/pkg/kube/site/bindings_test.go b/pkg/kube/site/bindings_test.go
index 3c2333ac..1fb1cca8 100644
--- a/pkg/kube/site/bindings_test.go
+++ b/pkg/kube/site/bindings_test.go
@@ -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 faa56313..410c1695 100644
--- a/pkg/kube/site/site_test.go
+++ b/pkg/kube/site/site_test.go
@@ -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/nonkube/api/environment_test.go b/pkg/nonkube/api/environment_test.go
index e77cfb70..544ed250 100644
--- a/pkg/nonkube/api/environment_test.go
+++ b/pkg/nonkube/api/environment_test.go
@@ -9,8 +9,8 @@ import (
 	"testing"
 
 	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/internal/config"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"github.com/skupperproject/skupper/pkg/config"
 	"gopkg.in/yaml.v3"
 	"gotest.tools/v3/assert"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
diff --git a/pkg/site/bindings_test.go b/pkg/site/bindings_test.go
index 404607a0..846aed3e 100644
--- a/pkg/site/bindings_test.go
+++ b/pkg/site/bindings_test.go
@@ -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)
 		}
 	}
 }