Repository: skupperproject/skupper
Pull request #1249 (2023-10-06 21:05:52)

fix update


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index a0ad1ec9..092a0174 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -104,7 +104,7 @@ commands:
           command: sudo rm -rf /usr/local/go
       - docker/install-docker
       - go/install:
-          version: "1.19"
+          version: "1.20"
       - kube-orb/install-kubectl
       - run: make
 
@@ -156,7 +156,10 @@ commands:
         name: vmstat info
         command: vmstat -w -t 10
         background: true
-
+    - run:
+        name: journalctl monitoring
+        command: journalctl -p 7 -b -f
+        background: true
   system_status:
     description: shows some point-in-time system status
     parameters:
@@ -195,6 +198,7 @@ commands:
           command: |
             make build-cmd && sudo install skupper /usr/local/bin
             export PUBLIC_1_INGRESS_HOST=`minikube ip`
+            export SKUPPER_TEST_CLI_MAX_STATUS_ATTEMPTS=10
             go test -tags=integration,podman -timeout=60m -v ./test/integration/... | tee /tmp/results/test-integration.out
       - run:
           name: Run client tests in real cluster
@@ -307,6 +311,10 @@ commands:
             sudo apt-get -qq -y install podman
             podman version
 
+            # Bypassing CircleCI issue with user session (see: https://github.com/containers/podman/issues/16529)
+            mkdir -p ~/.config/containers/containers.conf.d
+            ( echo '[containers]' ; echo 'cgroups = "disabled"' ) > ~/.config/containers/containers.conf.d/podman-circleci-issue.conf
+
             # Starting systemd user service
             systemctl --user start podman.socket
 
@@ -404,7 +412,7 @@ jobs:
   test:
     executor:
       name: go/default
-      tag: "1.19"
+      tag: "1.20"
     steps:
       - checkout
       - run:
@@ -450,7 +458,7 @@ jobs:
   build-all:
     executor:
       name: go/default
-      tag: "1.19"
+      tag: "1.20"
     steps:
       - checkout
       - go/mod-download-cached
@@ -534,7 +542,7 @@ jobs:
   generate-manifest:
     executor:
       name: go/default
-      tag: "1.19"
+      tag: "1.20"
     steps:
       - checkout
       - go/mod-download-cached
@@ -574,7 +582,7 @@ jobs:
   publish-github-release-images:
     executor:
       name: go/default
-      tag: "1.19"
+      tag: "1.20"
     steps:
       - checkout
       - setup_remote_docker
@@ -595,7 +603,7 @@ jobs:
   publish-github-main-images:
     executor:
       name: go/default
-      tag: "1.19"
+      tag: "1.20"
     steps:
       - checkout
       - setup_remote_docker
diff --git a/Dockerfile.config-sync b/Dockerfile.config-sync
index 62c74581..19331e4b 100644
--- a/Dockerfile.config-sync
+++ b/Dockerfile.config-sync
@@ -1,4 +1,4 @@
-FROM golang:1.19 AS builder
+FROM golang:1.20 AS builder
 
 WORKDIR /go/src/app
 
diff --git a/Dockerfile.controller-podman b/Dockerfile.controller-podman
index 9d959e27..6e687450 100644
--- a/Dockerfile.controller-podman
+++ b/Dockerfile.controller-podman
@@ -1,4 +1,4 @@
-FROM golang:1.19 AS builder
+FROM golang:1.20 AS builder
 
 WORKDIR /go/src/app
 COPY go.mod .
diff --git a/Dockerfile.flow-collector b/Dockerfile.flow-collector
index 7730f22c..2b88902e 100644
--- a/Dockerfile.flow-collector
+++ b/Dockerfile.flow-collector
@@ -1,4 +1,4 @@
-FROM golang:1.19 AS builder
+FROM golang:1.20 AS builder
 
 WORKDIR /go/src/app
 
diff --git a/Dockerfile.service-controller b/Dockerfile.service-controller
index f4b42946..45c31d31 100644
--- a/Dockerfile.service-controller
+++ b/Dockerfile.service-controller
@@ -1,4 +1,4 @@
-FROM golang:1.19 AS builder
+FROM golang:1.20 AS builder
 
 WORKDIR /go/src/app
 COPY go.mod .
diff --git a/Dockerfile.site-controller b/Dockerfile.site-controller
index 7a54054f..9fb0186f 100644
--- a/Dockerfile.site-controller
+++ b/Dockerfile.site-controller
@@ -1,4 +1,4 @@
-FROM golang:1.19 AS builder
+FROM golang:1.20 AS builder
 
 WORKDIR /go/src/app
 
diff --git a/api/types/client.go b/api/types/client.go
index 1a88e627..b44c4918 100644
--- a/api/types/client.go
+++ b/api/types/client.go
@@ -188,6 +188,9 @@ func (s *SiteConfigSpec) IsConsoleIngressKubernetes() bool {
 func (s *SiteConfigSpec) IsConsoleIngressNone() bool {
 	return s.getConsoleIngress() == IngressNoneString
 }
+func (s *SiteConfigSpec) IsEdge() bool {
+	return s.RouterMode == string(TransportModeEdge)
+}
 func (s *SiteConfigSpec) getConsoleIngress() string {
 	if s.ConsoleIngress == "" {
 		return s.Ingress
diff --git a/api/types/types.go b/api/types/types.go
index 1991d536..a926ddcc 100644
--- a/api/types/types.go
+++ b/api/types/types.go
@@ -399,6 +399,7 @@ type DeploymentSpec struct {
 	MemoryRequest       *resource.Quantity           `json:"memoryRequest,omitempty"`
 	CpuLimit            *resource.Quantity           `json:"cpuLimit,omitempty"`
 	MemoryLimit         *resource.Quantity           `json:"memoryLimit,omitempty"`
+	HostAliases         []corev1.HostAlias           `json:"hostAliases,omitempty"`
 }
 
 func (s *DeploymentSpec) GetCpuRequest() resource.Quantity {
diff --git a/client/revoke_all.go b/client/revoke_all.go
index 33e985fc..15d9d900 100644
--- a/client/revoke_all.go
+++ b/client/revoke_all.go
@@ -2,6 +2,7 @@ package client
 
 import (
 	"context"
+	"fmt"
 
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -35,18 +36,21 @@ func (cli *VanClient) appendControllerIngressHost(cred *types.Credential) bool {
 	return false
 }
 
-func (cli *VanClient) regenerateSiteSecret(ctx context.Context, ca *corev1.Secret) error {
+func (cli *VanClient) regenerateSiteSecret(ctx context.Context, ca *corev1.Secret, namespace string) error {
 	siteServerSecret := types.Credential{
 		Name:    types.SiteServerSecret,
 		Subject: types.TransportServiceName,
-		Hosts:   []string{types.TransportServiceName + "." + cli.Namespace},
+		Hosts:   []string{types.TransportServiceName + "." + namespace},
 	}
 
-	siteconfig, err := cli.SiteConfigInspect(ctx, nil)
+	siteconfig, err := cli.SiteConfigInspectInNamespace(ctx, nil, namespace)
 	if err != nil {
 		return err
 	}
-	rslvr, err := resolver.NewResolver(cli, cli.Namespace, &siteconfig.Spec)
+	if siteconfig == nil {
+		return fmt.Errorf("No site found in %s", namespace)
+	}
+	rslvr, err := resolver.NewResolver(cli, namespace, &siteconfig.Spec)
 	if err != nil {
 		return err
 	}
@@ -55,11 +59,11 @@ func (cli *VanClient) regenerateSiteSecret(ctx context.Context, ca *corev1.Secre
 		return err
 	}
 	siteServerSecret.Hosts = append(siteServerSecret.Hosts, hosts...)
-	_, err = kube.RegenerateCredentials(siteServerSecret, cli.Namespace, ca, cli.KubeClient)
+	_, err = kube.RegenerateCredentials(siteServerSecret, namespace, ca, cli.KubeClient)
 	if err != nil {
 		return err
 	}
-	return cli.restartRouter(cli.Namespace)
+	return cli.restartRouter(namespace)
 }
 
 func (cli *VanClient) RevokeAccess(ctx context.Context) error {
@@ -78,5 +82,5 @@ func (cli *VanClient) RevokeAccess(ctx context.Context) error {
 	if err != nil {
 		return err
 	}
-	return cli.regenerateSiteSecret(ctx, ca)
+	return cli.regenerateSiteSecret(ctx, ca, cli.Namespace)
 }
diff --git a/client/router_create.go b/client/router_create.go
index 74e682c6..e303da71 100644
--- a/client/router_create.go
+++ b/client/router_create.go
@@ -5,6 +5,7 @@ import (
 	"context"
 	"fmt"
 	"log"
+	"net"
 	"strconv"
 	"strings"
 	"text/template"
@@ -22,6 +23,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	kubetypes "k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/intstr"
+	"k8s.io/apimachinery/pkg/util/validation"
 
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/pkg/kube"
@@ -716,7 +718,7 @@ func (cli *VanClient) GetRouterSpecFromOpts(options types.SiteConfigSpec, siteId
 		fmt.Println("Error configuring router:", err)
 	}
 
-	isEdge := options.RouterMode == string(types.TransportModeEdge)
+	isEdge := options.IsEdge()
 	routerConfig := qdr.InitialConfigSkupperRouter(van.Name+"-${HOSTNAME}", siteId, version.Version, isEdge, 3, options.Router)
 	van.RouterConfig, _ = qdr.MarshalRouterConfig(routerConfig)
 
@@ -1083,6 +1085,63 @@ func (cli *VanClient) GetRouterSpecFromOpts(options types.SiteConfigSpec, siteId
 	return van
 }
 
+func (cli *VanClient) GetRouterHostAliasesSpecFromTokens(ctx context.Context, namespace string) ([]corev1.HostAlias, error) {
+	hostAliasesMap := make(map[string]map[string]bool)
+	secrets, err := cli.KubeClient.CoreV1().Secrets(namespace).List(ctx, metav1.ListOptions{LabelSelector: types.SkupperTypeQualifier + "=" + types.TypeToken})
+	if err != nil {
+		return nil, fmt.Errorf("Failed to retrieve connection token: %w", err)
+	}
+	if len(secrets.Items) == 0 {
+		return nil, nil
+	} else {
+		for _, s := range secrets.Items {
+			if alias, ok := s.ObjectMeta.Annotations["edge-alias"]; ok {
+				if net.ParseIP(alias) == nil {
+					log.Printf("Skipping edge-alias: %s is not a valid textual representation of an IP address\n", alias)
+				} else {
+					name := s.ObjectMeta.Annotations["edge-host"]
+					if msgs := validation.IsDNS1123Subdomain(name); len(msgs) != 0 {
+						log.Printf("Skipping edge-alias: %v\n", msgs)
+					} else {
+						if _, ok := hostAliasesMap[alias][name]; !ok {
+							hostAliasesMap[alias] = make(map[string]bool)
+							hostAliasesMap[alias][name] = true
+						}
+					}
+				}
+			}
+			if alias, ok := s.ObjectMeta.Annotations["inter-router-alias"]; ok {
+				if net.ParseIP(alias) == nil {
+					log.Printf("Skipping inter-router-alias: %s is not a valid textual representation of an IP address", alias)
+				} else {
+					name := s.ObjectMeta.Annotations["inter-router-host"]
+					if msgs := validation.IsDNS1123Subdomain(name); len(msgs) != 0 {
+						log.Printf("Skipping inter-router-alias: %v\n", msgs)
+					} else {
+						if _, ok := hostAliasesMap[alias][name]; !ok {
+							hostAliasesMap[alias] = make(map[string]bool)
+							hostAliasesMap[alias][name] = true
+						}
+					}
+				}
+			}
+		}
+	}
+	hostAliases := []corev1.HostAlias{}
+	for alias, v := range hostAliasesMap {
+		new := corev1.HostAlias{
+			IP:        alias,
+			Hostnames: []string{},
+		}
+		for hn, _ := range v {
+			new.Hostnames = append(new.Hostnames, hn)
+		}
+		hostAliases = append(hostAliases, new)
+	}
+
+	return hostAliases, nil
+}
+
 // RouterCreate instantiates a VAN (router and controller) deployment
 func (cli *VanClient) RouterCreate(ctx context.Context, options types.SiteConfig) error {
 	// todo return error
@@ -1120,6 +1179,11 @@ func (cli *VanClient) RouterCreate(ctx context.Context, options types.SiteConfig
 		ownerRefs = []metav1.OwnerReference{*siteOwnerRef}
 	}
 	var err error
+	hostAliases, err := cli.GetRouterHostAliasesSpecFromTokens(ctx, cli.GetNamespace())
+	if err != nil {
+		return err
+	}
+	van.Transport.HostAliases = hostAliases
 	if options.Spec.AuthMode == string(types.ConsoleAuthModeInternal) {
 		config := `
 pwcheck_method: auxprop
diff --git a/client/router_update.go b/client/router_update.go
index c08ca63b..48a98ca5 100644
--- a/client/router_update.go
+++ b/client/router_update.go
@@ -10,9 +10,11 @@ import (
 	"os"
 	"os/exec"
 	"reflect"
+	"sort"
 	"strings"
 	"time"
 
+	"github.com/google/go-cmp/cmp"
 	"github.com/skupperproject/skupper/pkg/images"
 	"github.com/skupperproject/skupper/pkg/version"
 	appsv1 "k8s.io/api/apps/v1"
@@ -462,10 +464,6 @@ func (cli *VanClient) RouterUpdateVersionInNamespace(ctx context.Context, hup bo
 		updateRouter = true
 	}
 	if addClaimsSupport {
-		err = kube.UpdateRole(namespace, types.TransportRoleName, types.TransportPolicyRule, cli.KubeClient)
-		if err != nil {
-			return false, err
-		}
 		if !config.IsEdge() {
 			if usingRoutes {
 				err = cli.createClaimsRedemptionRoute(ctx, namespace)
@@ -476,6 +474,10 @@ func (cli *VanClient) RouterUpdateVersionInNamespace(ctx context.Context, hup bo
 		}
 	}
 	if moveClaims {
+		err = kube.UpdateRole(namespace, types.TransportRoleName, types.TransportPolicyRule, cli.KubeClient)
+		if err != nil {
+			return false, err
+		}
 		if err := cli.moveClaimsToRouterService(ctx, namespace); err != nil {
 			return false, err
 		}
@@ -1084,6 +1086,56 @@ func (cli *VanClient) RouterUpdateAnnotations(ctx context.Context, settings *cor
 	return updated, nil
 }
 
+func sortHostAliases(hostAliases []corev1.HostAlias) []corev1.HostAlias {
+	sort.Slice(hostAliases, func(i, j int) bool {
+		return hostAliases[i].IP < hostAliases[j].IP
+	})
+
+	for _, hn := range hostAliases {
+		sort.Slice(hn.Hostnames, func(i, j int) bool {
+			return hn.Hostnames[i] < hn.Hostnames[j]
+		})
+	}
+	return hostAliases
+}
+
+func hostAliasesEqual(new, old []corev1.HostAlias) bool {
+	if len(new) != len(old) {
+		return false
+	}
+	return cmp.Equal(sortHostAliases(new), sortHostAliases(old))
+}
+
+func (cli *VanClient) updateHostAliasesOnDeployment(ctx context.Context, namespace string, name string, hostAliases []corev1.HostAlias) (bool, error) {
+	deployment, err := cli.KubeClient.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{})
+	if err != nil {
+		return false, err
+	}
+
+	if !hostAliasesEqual(hostAliases, deployment.Spec.Template.Spec.HostAliases) {
+		deployment.Spec.Template.Spec.HostAliases = hostAliases
+		_, err = cli.KubeClient.AppsV1().Deployments(namespace).Update(ctx, deployment, metav1.UpdateOptions{})
+		if err != nil {
+			return false, err
+		}
+		return true, nil
+	}
+	return false, nil
+}
+
+func (cli *VanClient) RouterUpdateHostAliases(ctx context.Context, token *corev1.Secret) (bool, error) {
+	hostAliases, err := cli.GetRouterHostAliasesSpecFromTokens(ctx, token.ObjectMeta.Namespace)
+	if err != nil {
+		return false, err
+	}
+
+	updated, err := cli.updateHostAliasesOnDeployment(ctx, token.Namespace, types.TransportDeploymentName, hostAliases)
+	if err != nil {
+		return updated, err
+	}
+	return updated, nil
+}
+
 func (cli *VanClient) RouterRestart(ctx context.Context, namespace string) error {
 	router, err := cli.KubeClient.AppsV1().Deployments(namespace).Get(ctx, types.TransportDeploymentName, metav1.GetOptions{})
 	if err != nil {
@@ -1247,7 +1299,7 @@ func (cli *VanClient) moveClaimsToRouterService(ctx context.Context, namespace s
 	if err != nil {
 		return err
 	}
-	err = cli.regenerateSiteSecret(ctx, ca)
+	err = cli.regenerateSiteSecret(ctx, ca, namespace)
 	if err != nil {
 		return err
 	}
diff --git a/cmd/site-controller/controller.go b/cmd/site-controller/controller.go
index d4c05a93..66ab9a01 100644
--- a/cmd/site-controller/controller.go
+++ b/cmd/site-controller/controller.go
@@ -26,6 +26,7 @@ import (
 type SiteController struct {
 	vanClient            *client.VanClient
 	siteInformer         cache.SharedIndexInformer
+	tokenInformer        cache.SharedIndexInformer
 	tokenRequestInformer cache.SharedIndexInformer
 	workqueue            workqueue.RateLimitingInterface
 }
@@ -52,6 +53,14 @@ func NewSiteController(cli *client.VanClient) (*SiteController, error) {
 			options.FieldSelector = "metadata.name=skupper-site"
 			options.LabelSelector = "!" + types.SiteControllerIgnore
 		}))
+	tokenInformer := corev1informer.NewFilteredSecretInformer(
+		cli.KubeClient,
+		watchNamespace,
+		time.Second*30,
+		cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
+		internalinterfaces.TweakListOptionsFunc(func(options *metav1.ListOptions) {
+			options.LabelSelector = types.TypeTokenQualifier
+		}))
 	tokenRequestInformer := corev1informer.NewFilteredSecretInformer(
 		cli.KubeClient,
 		watchNamespace,
@@ -65,11 +74,13 @@ func NewSiteController(cli *client.VanClient) (*SiteController, error) {
 	controller := &SiteController{
 		vanClient:            cli,
 		siteInformer:         siteInformer,
+		tokenInformer:        tokenInformer,
 		tokenRequestInformer: tokenRequestInformer,
 		workqueue:            workqueue,
 	}
 
 	siteInformer.AddEventHandler(controller.getHandlerFuncs(SiteConfig, configmapResourceVersionTest))
+	tokenInformer.AddEventHandler(controller.getHandlerFuncs(Token, secretResourceVersionTest))
 	tokenRequestInformer.AddEventHandler(controller.getHandlerFuncs(TokenRequest, secretResourceVersionTest))
 	return controller, nil
 }
@@ -110,6 +121,7 @@ func (c *SiteController) Run(stopCh <-chan struct{}) error {
 
 	log.Println("Starting the Skupper site controller informers")
 	go c.siteInformer.Run(stopCh)
+	go c.tokenInformer.Run(stopCh)
 	go c.tokenRequestInformer.Run(stopCh)
 
 	log.Println("Waiting for informer caches to sync")
@@ -175,6 +187,8 @@ func (c *SiteController) dispatchTrigger(trigger trigger) error {
 	switch trigger.category {
 	case SiteConfig:
 		return c.checkSite(trigger.key)
+	case Token:
+		return c.checkToken(trigger.key)
 	case TokenRequest:
 		return c.checkTokenRequest(trigger.key)
 	default:
@@ -199,6 +213,7 @@ func (c *SiteController) enqueueTrigger(obj interface{}, category triggerType) {
 func (c *SiteController) checkAllForSite() {
 	// Now need to check whether there are any token requests already in place
 	log.Println("Checking token requests...")
+	c.checkAllToken()
 	c.checkAllTokenRequests()
 	log.Println("Done.")
 }
@@ -332,6 +347,15 @@ func (c *SiteController) generate(token *corev1.Secret) error {
 	}
 }
 
+func (c *SiteController) checkAllToken() {
+	// can we rely on the cache here?
+	tokens := c.tokenInformer.GetStore().List()
+	for _, t := range tokens {
+		// service from workqueue
+		c.enqueueTrigger(t, Token)
+	}
+}
+
 func (c *SiteController) checkAllTokenRequests() {
 	// can we rely on the cache here?
 	tokens := c.tokenRequestInformer.GetStore().List()
@@ -341,6 +365,32 @@ func (c *SiteController) checkAllTokenRequests() {
 	}
 }
 
+func (c *SiteController) checkToken(key string) error {
+	log.Printf("Handling token for %s", key)
+	obj, exists, err := c.tokenInformer.GetStore().GetByKey(key)
+	if err != nil {
+		log.Println("Error checking connection-token secret: ", err)
+		return err
+	}
+	if !exists {
+		return nil
+	}
+
+	token := obj.(*corev1.Secret)
+	if !c.isTokenValidInSite(token) {
+		log.Println("Cannot handle token, as site not yet initialised")
+		return nil
+	}
+
+	updated, _ := c.vanClient.RouterUpdateHostAliases(context.Background(), token)
+	if updated {
+		log.Printf("Router updated and restarted due to changes in token %s", key)
+	} else {
+		log.Printf("Changes in token %s didn't trigger router update", key)
+	}
+	return nil
+}
+
 func (c *SiteController) checkTokenRequest(key string) error {
 	log.Printf("Handling token request for %s", key)
 	obj, exists, err := c.tokenRequestInformer.GetStore().GetByKey(key)
diff --git a/cmd/skupper/skupper.go b/cmd/skupper/skupper.go
index 1d35ab31..f1bb80a6 100644
--- a/cmd/skupper/skupper.go
+++ b/cmd/skupper/skupper.go
@@ -126,6 +126,8 @@ type BindOptions struct {
 	PublishNotReadyAddresses bool
 	tlsCertAuthority         string
 	Namespace                string
+	Headless                 bool
+	ProxyTuning              types.Tuning
 }
 
 func SkupperNotInstalledError(namespace string) error {
diff --git a/cmd/skupper/skupper_kube_service.go b/cmd/skupper/skupper_kube_service.go
index 2147b61a..cee22d02 100644
--- a/cmd/skupper/skupper_kube_service.go
+++ b/cmd/skupper/skupper_kube_service.go
@@ -158,6 +158,34 @@ func (s *SkupperKubeService) Bind(cmd *cobra.Command, args []string) error {
 		return targetTypeServiceTargetNamespaceError()
 	}
 
+	if bindOptions.Headless && targetType != "statefulset" {
+		return fmt.Errorf("--headless option is only valid for statefulsets")
+	}
+
+	if !bindOptions.Headless {
+		if bindOptions.ProxyTuning.Cpu != "" {
+			return fmt.Errorf("--proxy-cpu option is only valid for binding statefulsets using headless services")
+		}
+		if bindOptions.ProxyTuning.Memory != "" {
+			return fmt.Errorf("--proxy-memory option is only valid for binding statefulsets using headless services")
+		}
+		if bindOptions.ProxyTuning.CpuLimit != "" {
+			return fmt.Errorf("--proxy-cpu-limit option is only valid for binding statefulsets using headless services")
+		}
+		if bindOptions.ProxyTuning.MemoryLimit != "" {
+			return fmt.Errorf("--proxy-memory-limit option is only valid for binding statefulsets using headless services")
+		}
+		if bindOptions.ProxyTuning.Affinity != "" {
+			return fmt.Errorf("--proxy-pod-affinity option is only valid for binding statefulsets using headless services")
+		}
+		if bindOptions.ProxyTuning.AntiAffinity != "" {
+			return fmt.Errorf("--proxy-pod-antiaffinity option is only valid for binding statefulsets using headless services")
+		}
+		if bindOptions.ProxyTuning.NodeSelector != "" {
+			return fmt.Errorf("--proxy-node-selector option is only valid for binding statefulsets using headless services")
+		}
+	}
+
 	service, err := s.kube.Cli.ServiceInterfaceInspect(context.Background(), args[0])
 
 	if err != nil {
@@ -166,6 +194,26 @@ func (s *SkupperKubeService) Bind(cmd *cobra.Command, args []string) error {
 		return fmt.Errorf("Service %s not found", args[0])
 	}
 
+	if targetType == "statefulset" {
+
+		if bindOptions.Headless {
+			err := s.kube.Cli.ServiceInterfaceRemove(context.Background(), service.Address)
+			if err != nil {
+				return err
+			}
+
+			service, err = s.kube.Cli.GetHeadlessServiceConfiguration(targetName, service.Protocol, service.Address, service.Ports, bindOptions.PublishNotReadyAddresses, bindOptions.Namespace)
+			if err != nil {
+				return err
+			}
+
+			err = configureHeadlessProxy(service.Headless, &bindOptions.ProxyTuning)
+
+			return s.kube.Cli.ServiceInterfaceUpdate(context.Background(), service)
+		}
+
+	}
+
 	// validating ports
 	portMapping, err := parsePortMapping(service, bindOptions.TargetPorts)
 	if err != nil {
@@ -191,6 +239,14 @@ func (s *SkupperKubeService) BindArgs(cmd *cobra.Command, args []string) error {
 func (s *SkupperKubeService) BindFlags(cmd *cobra.Command) {
 	cmd.Flags().BoolVar(&bindOptions.PublishNotReadyAddresses, "publish-not-ready-addresses", false, "If specified, skupper will not wait for pods to be ready")
 	cmd.Flags().StringVar(&bindOptions.Namespace, "target-namespace", "", "Expose resources from a specific namespace")
+	cmd.Flags().BoolVar(&bindOptions.Headless, "headless", false, "Bind through a headless service (valid only for a statefulset target)")
+	cmd.Flags().StringVar(&bindOptions.ProxyTuning.Cpu, "proxy-cpu", "", "CPU request for router pods")
+	cmd.Flags().StringVar(&bindOptions.ProxyTuning.Memory, "proxy-memory", "", "Memory request for router pods")
+	cmd.Flags().StringVar(&bindOptions.ProxyTuning.CpuLimit, "proxy-cpu-limit", "", "CPU limit for router pods")
+	cmd.Flags().StringVar(&bindOptions.ProxyTuning.MemoryLimit, "proxy-memory-limit", "", "Memory limit for router pods")
+	cmd.Flags().StringVar(&bindOptions.ProxyTuning.NodeSelector, "proxy-node-selector", "", "Node selector to control placement of router pods")
+	cmd.Flags().StringVar(&bindOptions.ProxyTuning.Affinity, "proxy-pod-affinity", "", "Pod affinity label matches to control placement of router pods")
+	cmd.Flags().StringVar(&bindOptions.ProxyTuning.AntiAffinity, "proxy-pod-antiaffinity", "", "Pod antiaffinity label matches to control placement of router pods")
 }
 
 func (s *SkupperKubeService) Unbind(cmd *cobra.Command, args []string) error {
diff --git a/go.mod b/go.mod
index d909d45e..6e3582e3 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
 module github.com/skupperproject/skupper
 
-go 1.19
+go 1.20
 
 require (
 	github.com/davecgh/go-spew v1.1.1
diff --git a/go.sum b/go.sum
index 9fe5e6ef..21c288cb 100644
--- a/go.sum
+++ b/go.sum
@@ -127,7 +127,6 @@ github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD
 github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
 github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
-github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
 github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
 github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
 github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
diff --git a/pkg/domain/link.go b/pkg/domain/link.go
index cc85726f..ec5fa0af 100644
--- a/pkg/domain/link.go
+++ b/pkg/domain/link.go
@@ -110,10 +110,14 @@ func (c *ClaimRedeemer) RedeemClaim(claim *corev1.Secret) error {
 	request.URL.RawQuery = query.Encode()
 	response, err := client.Do(request)
 	if err != nil {
+		fmt.Printf("Claim request failed on invoke: %s", err)
+		fmt.Println()
 		return c.handleError(claim, err.Error(), false)
 	}
 	body, err := ioutil.ReadAll(response.Body)
 	if err != nil {
+		fmt.Printf("Claim request failed on reading body: %s", err)
+		fmt.Println()
 		return c.handleError(claim, err.Error(), false)
 	}
 	if response.StatusCode != http.StatusOK {
diff --git a/pkg/flow/flow_mem_driver.go b/pkg/flow/flow_mem_driver.go
index 9f8206ac..e67eb1c8 100644
--- a/pkg/flow/flow_mem_driver.go
+++ b/pkg/flow/flow_mem_driver.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"log"
+	"net"
 	"strings"
 	"time"
 
@@ -755,8 +756,8 @@ func (fc *FlowCollector) updateRecord(record interface{}) error {
 			} else {
 				if connector.EndTime > 0 {
 					current.EndTime = connector.EndTime
-					if current.process != nil {
-						if process, ok := fc.Processes[*current.process]; ok {
+					if current.ProcessId != nil {
+						if process, ok := fc.Processes[*current.ProcessId]; ok {
 							process.connector = nil
 							process.ProcessBinding = &Unbound
 						}
@@ -1337,8 +1338,8 @@ func (fc *FlowCollector) retrieve(request ApiRequest) (*string, error) {
 		case "process":
 			if id, ok := vars["id"]; ok {
 				if connector, ok := fc.Connectors[id]; ok {
-					if connector.process != nil {
-						if process, ok := fc.Processes[*connector.process]; ok {
+					if connector.ProcessId != nil {
+						if process, ok := fc.Processes[*connector.ProcessId]; ok {
 							p.Count = 1
 							p.Results = *process
 						}
@@ -1448,8 +1449,8 @@ func (fc *FlowCollector) retrieve(request ApiRequest) (*string, error) {
 			if id, ok := vars["id"]; ok {
 				if vanaddr, ok := fc.VanAddresses[id]; ok {
 					for _, connector := range fc.Connectors {
-						if *connector.Address == vanaddr.Name && connector.process != nil {
-							if process, ok := fc.Processes[*connector.process]; ok {
+						if *connector.Address == vanaddr.Name && connector.ProcessId != nil {
+							if process, ok := fc.Processes[*connector.ProcessId]; ok {
 								if filterRecord(*process, queryParams) && process.Base.TimeRangeValid(queryParams) {
 									unique[process.Identity] = process
 								}
@@ -1468,10 +1469,10 @@ func (fc *FlowCollector) retrieve(request ApiRequest) (*string, error) {
 			if id, ok := vars["id"]; ok {
 				if vanaddr, ok := fc.VanAddresses[id]; ok {
 					for _, connector := range fc.Connectors {
-						if *connector.Address == vanaddr.Name && connector.process != nil {
+						if *connector.Address == vanaddr.Name && connector.ProcessId != nil {
 							for _, aggregate := range fc.FlowAggregates {
 								if aggregate.PairType == recordNames[Process] {
-									if *connector.process == *aggregate.DestinationId {
+									if *connector.ProcessId == *aggregate.DestinationId {
 										p.TotalCount++
 										if filterRecord(*aggregate, queryParams) {
 											processPairs = append(processPairs, *aggregate)
@@ -1551,7 +1552,7 @@ func (fc *FlowCollector) retrieve(request ApiRequest) (*string, error) {
 			if id, ok := vars["id"]; ok {
 				if _, ok := fc.Processes[id]; ok {
 					for _, connector := range fc.Connectors {
-						if connector.process != nil && *connector.process == id {
+						if connector.ProcessId != nil && *connector.ProcessId == id {
 							for _, address := range fc.VanAddresses {
 								if *connector.Address == address.Name {
 									if filterRecord(*address, queryParams) {
@@ -2070,8 +2071,8 @@ func (fc *FlowCollector) reconcileRecords() error {
 			if flow.SourceHost != nil {
 				if connector, ok := fc.Connectors[flow.Parent]; ok {
 					flow.Place = serverSide
-					if connector.process != nil && connector.AddressId != nil {
-						flow.Process = connector.process
+					if connector.ProcessId != nil && connector.AddressId != nil {
+						flow.Process = connector.ProcessId
 						if process, ok := fc.Processes[*flow.Process]; ok {
 							flow.ProcessName = process.Name
 						}
@@ -2144,12 +2145,21 @@ func (fc *FlowCollector) reconcileRecords() error {
 				delete(fc.connectorsToReconcile, connId)
 			} else if connector.DestHost != nil {
 				siteId := fc.getRecordSiteId(*connector)
+				var matchHost *string
 				found := false
+				if net.ParseIP(*connector.DestHost) == nil {
+					addrs, err := net.LookupHost(*connector.DestHost)
+					if err == nil && len(addrs) > 0 {
+						matchHost = &addrs[0]
+					}
+				} else {
+					matchHost = connector.DestHost
+				}
 				for _, process := range fc.Processes {
 					if siteId == process.Parent {
 						if process.SourceHost != nil {
-							if *connector.DestHost == *process.SourceHost {
-								connector.process = &process.Identity
+							if *matchHost == *process.SourceHost {
+								connector.ProcessId = &process.Identity
 								process.connector = &connector.Identity
 								process.ProcessBinding = &Bound
 								log.Printf("COLLECTOR: Connector %s/%s associated to process %s\n", connector.Identity, *connector.Address, *process.Name)
@@ -2174,7 +2184,7 @@ func (fc *FlowCollector) reconcileRecords() error {
 						for _, process := range fc.Processes {
 							if process.Name != nil && *process.Name == processName {
 								log.Printf("COLLECTOR: Associating connector %s to external process %s\n", connector.Identity, processName)
-								connector.process = &process.Identity
+								connector.ProcessId = &process.Identity
 								delete(fc.connectorsToReconcile, connId)
 								break
 							}
diff --git a/pkg/flow/record.go b/pkg/flow/record.go
index a1b26dbd..6ac887b2 100644
--- a/pkg/flow/record.go
+++ b/pkg/flow/record.go
@@ -283,7 +283,7 @@ type ConnectorRecord struct {
 	FlowRateL4  *uint64 `json:"flowRateL4,omitempty"`
 	FlowCountL7 *uint64 `json:"flowCountL7,omitempty"`
 	FlowRateL7  *uint64 `json:"flowRateL7,omitempty"`
-	process     *string
+	ProcessId   *string `json:"processId,omitempty"`
 	AddressId   *string `json:"addressId,omitempty"`
 }
 
diff --git a/pkg/kube/claims/claim_verifier.go b/pkg/kube/claims/claim_verifier.go
index fa189012..54e379ff 100644
--- a/pkg/kube/claims/claim_verifier.go
+++ b/pkg/kube/claims/claim_verifier.go
@@ -21,7 +21,6 @@ import (
 	"k8s.io/client-go/util/retry"
 
 	"github.com/skupperproject/skupper/api/types"
-	"github.com/skupperproject/skupper/pkg/event"
 )
 
 const (
@@ -53,10 +52,12 @@ func newClaimVerifier(client kubernetes.Interface, namespace string, generator T
 }
 
 func (server *ClaimVerifier) checkAndUpdateClaim(name string, data []byte) (string, int) {
+	log.Printf("Checking claim %s", name)
 	claim, err := server.client.CoreV1().Secrets(server.namespace).Get(context.TODO(), name, metav1.GetOptions{})
 	if errors.IsNotFound(err) {
 		return "No such claim", http.StatusNotFound
 	} else if err != nil {
+		log.Printf("Error retrieving claim: %s", err)
 		return err.Error(), http.StatusInternalServerError
 	}
 	if claim.ObjectMeta.Labels == nil || claim.ObjectMeta.Labels[types.SkupperTypeQualifier] != types.TypeClaimRecord {
@@ -66,10 +67,10 @@ func (server *ClaimVerifier) checkAndUpdateClaim(name string, data []byte) (stri
 		if expirationString, ok := claim.ObjectMeta.Annotations[types.ClaimExpiration]; ok {
 			expiration, err := time.Parse(time.RFC3339, expirationString)
 			if err != nil {
-				event.Recordf(TokenClaimVerification, "Cannot determine expiration: %s", err)
+				log.Printf("Cannot determine expiration: %s", err)
 				return "Corrupted claim", http.StatusInternalServerError
 			} else if expiration.Before(time.Now()) {
-				event.Recordf(TokenClaimVerification, "Claim %s expired", name)
+				log.Printf("Claim %s expired", name)
 				return "No such claim", http.StatusNotFound
 			}
 		}
@@ -83,11 +84,11 @@ func (server *ClaimVerifier) checkAndUpdateClaim(name string, data []byte) (stri
 	if uses, ok := claim.ObjectMeta.Annotations[types.ClaimsRemaining]; ok {
 		remainingUses, err := strconv.Atoi(uses)
 		if err != nil {
-			event.Recordf(TokenClaimVerification, "Cannot determine remaining uses: %s", err)
+			log.Printf("Cannot determine remaining uses: %s", err)
 			return "Corrupted claim", http.StatusInternalServerError
 		}
 		if remainingUses == 0 {
-			event.Recordf(TokenClaimVerification, "Claim %s already used", name)
+			log.Printf("Claim %s already used", name)
 			return "No such claim", http.StatusNotFound
 		}
 		remainingUses -= 1
@@ -96,7 +97,7 @@ func (server *ClaimVerifier) checkAndUpdateClaim(name string, data []byte) (stri
 	if value, ok := claim.ObjectMeta.Annotations[types.ClaimsMade]; ok {
 		made, err := strconv.Atoi(value)
 		if err != nil {
-			event.Recordf(TokenClaimVerification, "Cannot determine claims made: %s", err)
+			log.Printf("Cannot determine claims made: %s", err)
 			return "Corrupted claim", http.StatusInternalServerError
 		}
 		made += 1
@@ -106,7 +107,7 @@ func (server *ClaimVerifier) checkAndUpdateClaim(name string, data []byte) (stri
 	}
 	_, err = server.client.CoreV1().Secrets(server.namespace).Update(context.TODO(), claim, metav1.UpdateOptions{})
 	if err != nil {
-		event.Recordf(TokenClaimVerification, "Error updating remaining uses: %s", err)
+		log.Printf("Error updating remaining uses: %s", err)
 		return "Internal error", http.StatusServiceUnavailable
 	}
 	return "ok", http.StatusOK
@@ -137,14 +138,14 @@ func (server *ClaimVerifier) redeemClaim(name string, subject string, data []byt
 
 func (server *ClaimVerifier) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	if r.Method != http.MethodPost {
-		event.Recordf(TokenClaimVerification, "Bad method %s", r.Method)
+		log.Printf("Bad method %s", r.Method)
 		http.Error(w, "Only POST is supported", http.StatusMethodNotAllowed)
 		return
 	}
 	name := strings.Join(strings.Split(r.URL.Path, "/"), "")
 	body, err := ioutil.ReadAll(r.Body)
 	if err != nil {
-		event.Record(TokenClaimVerification, err.Error())
+		log.Printf("Error reading body: %s", err.Error())
 		http.Error(w, "Request body not valid", http.StatusBadRequest)
 		return
 	}
@@ -158,24 +159,24 @@ func (server *ClaimVerifier) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 		if remoteSiteVersion == "" {
 			remoteSiteVersion = "undefined"
 		}
-		event.Recordf(TokenClaimVerification, "%s - remote site version is %s", err.Error(), remoteSiteVersion)
+		log.Printf("%s - remote site version is %s", err.Error(), remoteSiteVersion)
 		http.Error(w, err.Error(), http.StatusBadRequest)
 		return
 	}
 	token, text, code := server.redeemClaim(name, subject, body, server.generator)
 	if token == nil {
-		event.Recordf(TokenClaimVerification, "Claim request for %s failed: %s", name, text)
+		log.Printf("Claim request for %s failed: %s", name, text)
 		http.Error(w, text, code)
 		return
 	}
 	s := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme.Scheme, scheme.Scheme)
 	err = s.Encode(token, w)
 	if err != nil {
-		event.Record(TokenClaimVerification, err.Error())
+		log.Printf("Error encoding token: %s", err.Error())
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
 	}
-	event.Recordf(TokenClaimVerification, "Claim for %s succeeded", name)
+	log.Printf("Claim for %s succeeded", name)
 }
 
 const (
diff --git a/pkg/kube/deployments.go b/pkg/kube/deployments.go
index 360100da..c0380c69 100644
--- a/pkg/kube/deployments.go
+++ b/pkg/kube/deployments.go
@@ -3,6 +3,7 @@ package kube
 import (
 	"context"
 	"fmt"
+	"os"
 	"time"
 
 	"github.com/skupperproject/skupper/pkg/qdr"
@@ -129,7 +130,10 @@ func NewProxyStatefulSet(image types.ImageDetails, serviceInterface types.Servic
 
 	replicas := int32(serviceInterface.Headless.Size)
 	labels := map[string]string{
-		"internal.skupper.io/type": "proxy",
+		"internal.skupper.io/type":  "proxy",
+		"app.kubernetes.io/name":    "skupper-router",
+		"app.kubernetes.io/part-of": "skupper",
+		"skupper.io/component":      "router",
 	}
 	if len(transportDep.ObjectMeta.Labels) > 0 {
 		for k, v := range transportDep.ObjectMeta.Labels {
@@ -168,6 +172,9 @@ func NewProxyStatefulSet(image types.ImageDetails, serviceInterface types.Servic
 				ObjectMeta: metav1.ObjectMeta{
 					Labels: map[string]string{
 						"internal.skupper.io/service": serviceInterface.Address,
+						"app.kubernetes.io/name":      "skupper-router",
+						"app.kubernetes.io/part-of":   "skupper",
+						"skupper.io/component":        "router",
 					},
 				},
 				Spec: corev1.PodSpec{
@@ -187,7 +194,11 @@ func NewProxyStatefulSet(image types.ImageDetails, serviceInterface types.Servic
 									Value: "json",
 								},
 								{
-									Name: "NAMESPACE",
+									Name:  "SKUPPER_SITE_ID",
+									Value: os.Getenv("SKUPPER_SITE_ID"),
+								},
+								{
+									Name: "POD_NAMESPACE",
 									ValueFrom: &corev1.EnvVarSource{
 										FieldRef: &corev1.ObjectFieldSelector{
 											FieldPath: "metadata.namespace",
@@ -477,6 +488,7 @@ func NewTransportDeployment(van *types.RouterSpec, ownerRef *metav1.OwnerReferen
 						Containers: []corev1.Container{
 							ContainerForTransport(van.Transport),
 						},
+						HostAliases: van.Transport.HostAliases,
 					},
 				},
 			},
diff --git a/pkg/service_sync/service_sync.go b/pkg/service_sync/service_sync.go
index 2abfbc85..a08aa1e7 100644
--- a/pkg/service_sync/service_sync.go
+++ b/pkg/service_sync/service_sync.go
@@ -267,7 +267,7 @@ func getAddresses(services []types.ServiceInterface) []string {
 }
 
 func equivalentServiceDefinition(a *types.ServiceInterface, b *types.ServiceInterface) bool {
-	if a.Protocol != b.Protocol || !reflect.DeepEqual(a.Ports, b.Ports) || a.EventChannel != b.EventChannel || a.Aggregate != b.Aggregate || !reflect.DeepEqual(a.Labels, b.Labels) || !reflect.DeepEqual(a.Annotations, b.Annotations) {
+	if a.Protocol != b.Protocol || !reflect.DeepEqual(a.Ports, b.Ports) || a.EventChannel != b.EventChannel || a.Aggregate != b.Aggregate || !reflect.DeepEqual(a.Labels, b.Labels) || !reflect.DeepEqual(a.Annotations, b.Annotations) || a.TlsCredentials != b.TlsCredentials || a.TlsCertAuthority != b.TlsCertAuthority || a.PublishNotReadyAddresses != b.PublishNotReadyAddresses {
 		return false
 	}
 	if a.Headless == nil && b.Headless == nil {


Test patch:
diff --git a/Dockerfile.ci-test b/Dockerfile.ci-test
index eb134b6a..fb2e64f2 100644
--- a/Dockerfile.ci-test
+++ b/Dockerfile.ci-test
@@ -1,4 +1,4 @@
-FROM golang:1.19 AS builder
+FROM golang:1.20 AS builder
 
 WORKDIR /go/src/app
 
diff --git a/cmd/skupper/skupper_cluster_test.go b/cmd/skupper/skupper_cluster_test.go
index 8408a3eb..5b33794b 100644
--- a/cmd/skupper/skupper_cluster_test.go
+++ b/cmd/skupper/skupper_cluster_test.go
@@ -1354,13 +1354,77 @@ func TestBindWithCluster(t *testing.T) {
 			realCluster:     false,
 		},
 		{
-			doc:             "bind-test6",
+			doc:             "bind-test5",
 			args:            []string{"tcp-go-echo", "deployment", "tcp-go-echo", "--target-namespace", anotherNs},
 			expectedCapture: "",
 			expectedOutput:  "",
 			expectedError:   "",
 			realCluster:     true,
 		},
+		{
+			doc:             "bind-test6",
+			args:            []string{"web", "service", "web", "--headless"},
+			expectedCapture: "",
+			expectedOutput:  "",
+			expectedError:   "--headless option is only valid for statefulsets",
+			realCluster:     true,
+		},
+		{
+			doc:             "bind-test7",
+			args:            []string{"test", "statefulset", "test", "--proxy-cpu", "2"},
+			expectedCapture: "",
+			expectedOutput:  "",
+			expectedError:   "--proxy-cpu option is only valid for binding statefulsets using headless services",
+			realCluster:     true,
+		},
+		{
+			doc:             "bind-test8",
+			args:            []string{"test", "service", "test", "--proxy-memory", "2G"},
+			expectedCapture: "",
+			expectedOutput:  "",
+			expectedError:   "--proxy-memory option is only valid for binding statefulsets using headless services",
+			realCluster:     true,
+		},
+		{
+			doc:             "bind-test9",
+			args:            []string{"test", "statefulset", "test", "--proxy-cpu-limit", "2"},
+			expectedCapture: "",
+			expectedOutput:  "",
+			expectedError:   "--proxy-cpu-limit option is only valid for binding statefulsets using headless services",
+			realCluster:     true,
+		},
+		{
+			doc:             "bind-test10",
+			args:            []string{"test", "statefulset", "test", "--proxy-memory-limit", "2G"},
+			expectedCapture: "",
+			expectedOutput:  "",
+			expectedError:   "--proxy-memory-limit option is only valid for binding statefulsets using headless services",
+			realCluster:     true,
+		},
+		{
+			doc:             "bind-test11",
+			args:            []string{"test", "statefulset", "test", "--proxy-pod-affinity", "test"},
+			expectedCapture: "",
+			expectedOutput:  "",
+			expectedError:   "--proxy-pod-affinity option is only valid for binding statefulsets using headless services",
+			realCluster:     true,
+		},
+		{
+			doc:             "bind-test12",
+			args:            []string{"test", "statefulset", "test", "--proxy-pod-antiaffinity", "test"},
+			expectedCapture: "",
+			expectedOutput:  "",
+			expectedError:   "--proxy-pod-antiaffinity option is only valid for binding statefulsets using headless services",
+			realCluster:     true,
+		},
+		{
+			doc:             "bind-test13",
+			args:            []string{"test", "statefulset", "test", "--proxy-node-selector", "test"},
+			expectedCapture: "",
+			expectedOutput:  "",
+			expectedError:   "--proxy-node-selector option is only valid for binding statefulsets using headless services",
+			realCluster:     true,
+		},
 	}
 
 	namespace := "cmd-bind-cluster-test-" + strings.ToLower(utils.RandomId(4))
@@ -1604,65 +1668,3 @@ func TestDebugDumpWithCluster(t *testing.T) {
 		testCommand(t, cmd, tc.doc, tc.expectedError, tc.expectedCapture, tc.expectedOutput, tc.outputRegExp, tc.args...)
 	}
 }
-
-func TestNetworkStatusWithCluster(t *testing.T) {
-	testcases := []testCase{
-		{
-			doc:             "network-status-test1",
-			args:            []string{"--help"},
-			expectedCapture: "",
-			expectedOutput:  "Shows information about the current site, and connected sites.",
-			expectedError:   "",
-			realCluster:     true,
-		},
-		{
-			doc:             "network-status-test2",
-			args:            []string{"status"},
-			expectedCapture: "",
-			expectedOutput:  "",
-			expectedError:   "",
-			outputRegExp:    "^Sites\\:\\n╰─\\s\\[local\\]\\s[0-9a-f-]*\\s-\\s.*\\n\\s{3}URL:\\s\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\n\\s{3}mode:\\s.*\\n\\s{3}name:\\s.*\\n\\s{3}namespace:\\s.*\\n\\s{3}version:\\s.*\\n\\s{3}╰─\\sServices\\:\\n\\s{6}╰─\\sname:\\s.*\\n\\s{9}address:\\s.*\\:\\s\\d{4}\\n\\s{9}protocol:\\s.*\\n\\s{9}╰─\\sTargets\\:\\n\\s{12}╰─\\sname:\\s.*",
-			realCluster:     true,
-		},
-	}
-
-	namespace := "cmd-debug-network-status-cluster-test-" + strings.ToLower(utils.RandomId(4))
-
-	if !*clusterRun {
-		lightRed := "\033[1;31m"
-		resetColor := "\033[0m"
-		t.Skip(fmt.Sprintf("%sSkipping: This test only works in real clusters.%s", string(lightRed), string(resetColor)))
-	}
-
-	testClient = &SkupperTestClient{
-		SkupperKube: &SkupperKube{
-			Namespace: namespace,
-		},
-	}
-	testClient.NewClient(nil, nil)
-	cli := testClient.Cli
-
-	if c, ok := cli.(*client.VanClient); ok {
-		_, err := kube.NewNamespace(namespace, c.KubeClient)
-		assert.Check(t, err)
-		defer kube.DeleteNamespace(namespace, c.KubeClient)
-	}
-	skupperInit(t, []string{"--console-ingress=none"}...)
-	skupperExpose(t, []string{"service", "tcp-go-echo", "--port", "9090", "--address", "tcp-go-echo-dup"}...)
-
-	// wait for the service controller to be available
-	time.Sleep(15 * time.Second)
-
-	testPath := "./tmp/"
-	os.Mkdir(testPath, 0755)
-	defer os.RemoveAll(testPath)
-
-	for _, tc := range testcases {
-		if tc.realCluster && !*clusterRun {
-			continue
-		}
-		cmd := NewCmdNetworkStatus(testClient.Network())
-		silenceCobra(cmd)
-		testCommand(t, cmd, tc.doc, tc.expectedError, tc.expectedCapture, tc.expectedOutput, tc.outputRegExp, tc.args...)
-	}
-}
diff --git a/pkg/flow/flow_mem_driver_test.go b/pkg/flow/flow_mem_driver_test.go
index 22d6e6b3..1a1381e1 100644
--- a/pkg/flow/flow_mem_driver_test.go
+++ b/pkg/flow/flow_mem_driver_test.go
@@ -180,9 +180,9 @@ func TestRecordGraph(t *testing.T) {
 				Parent:    "router:0",
 				StartTime: uint64(time.Now().UnixNano()) / uint64(time.Microsecond),
 			},
-			Address:  &address3,
-			Protocol: &protocol,
-			process:  &connectorProcess1,
+			Address:   &address3,
+			Protocol:  &protocol,
+			ProcessId: &connectorProcess1,
 		},
 		{
 			Base: Base{
@@ -200,9 +200,9 @@ func TestRecordGraph(t *testing.T) {
 				Parent:    "router:1",
 				StartTime: uint64(time.Now().UnixNano()) / uint64(time.Microsecond),
 			},
-			Protocol: &protocol,
-			Address:  &address4,
-			process:  &connectorProcess2,
+			Protocol:  &protocol,
+			Address:   &address4,
+			ProcessId: &connectorProcess2,
 		},
 	}
 	flows := []FlowRecord{
diff --git a/test/integration/examples/custom/helloworld/helloworld_podman_test.go b/test/integration/examples/custom/helloworld/helloworld_podman_test.go
index d9cce381..4c60325e 100644
--- a/test/integration/examples/custom/helloworld/helloworld_podman_test.go
+++ b/test/integration/examples/custom/helloworld/helloworld_podman_test.go
@@ -3,10 +3,14 @@
 package helloworld
 
 import (
+	"bytes"
 	"fmt"
 	"log"
 	"os"
+	"os/exec"
+	"strings"
 	"testing"
+	"time"
 
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/client/container"
@@ -34,6 +38,9 @@ const (
 // It uses the kubernetes cluster as a public cluster and a podman site
 // as the private one.
 func TestHelloWorldCLIOnPodman(t *testing.T) {
+	stopCh := make(chan interface{}, 1)
+	go restartOnLinkDisconnected(stopCh, t)
+	defer close(stopCh)
 
 	// First, validate if skupper binary is in the PATH, or skip test
 	log.Printf("Running 'skupper --help' to determine if skupper binary is available")
@@ -173,7 +180,7 @@ func TestHelloWorldCLIOnPodman(t *testing.T) {
 					// skupper init - interior mode
 					&cli.InitTester{
 						Ingress:             "none",
-						RouterLogging:       "trace",
+						RouterLogging:       "info",
 						RouterMode:          "interior",
 						ConsoleAuth:         "internal",
 						ConsoleUser:         "internal",
@@ -675,6 +682,35 @@ func TestHelloWorldCLIOnPodman(t *testing.T) {
 
 }
 
+// restartOnLinkDisconnected On CircleCI external network connectivity
+// is being lost this routine will restart the skupper-podman user service
+// to bypass this issue.
+// TODO: this can be removed once CI is fixed
+func restartOnLinkDisconnected(stopCh chan interface{}, t *testing.T) {
+	if os.Getenv("USER") != "circleci" {
+		return
+	}
+	// Preparing the command to run
+	tick := time.NewTicker(time.Second * 5)
+	defer tick.Stop()
+	for {
+		select {
+		case <-tick.C:
+			stdout := new(bytes.Buffer)
+			cmd := exec.Command("skupper", "--platform=podman", "link", "status")
+			cmd.Stdout = stdout
+			_ = cmd.Run()
+			if strings.Contains(stdout.String(), " not connected") {
+				t.Logf("[Link Disconnected] - Restarting skupper-podman (user) service")
+				restartCmd := exec.Command("systemctl", "--user", "restart", "skupper-podman")
+				_ = restartCmd.Run()
+			}
+		case <-stopCh:
+			return
+		}
+	}
+}
+
 // deployPodmanResources runs the hello-world-backend as a podman container
 func deployPodmanResources() error {
 	podmanCli, err := clientpodman.NewPodmanClient("", "")