Repository: skupperproject/skupper
Pull request #1246 (2023-10-02 17:51:56)

apply specified labels to all created resources

grs: > lgtm
> 
> Suggest updating help text to indicate labels applied to all resource not just skupper pods: --labels strings Labels to add to skupper pods

Yes, I missed that but have fixed it now.

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..c8a69d60 100644
--- a/api/types/client.go
+++ b/api/types/client.go
@@ -17,7 +17,7 @@ type ConnectorCreateOptions struct {
 	SkupperNamespace string
 	Name             string
 	Cost             int32
-	Yaml             []byte
+	Secret           *corev1.Secret
 }
 
 type ConnectorRemoveOptions struct {
@@ -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..6a28f4c4 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 {
@@ -533,10 +534,12 @@ type Credential struct {
 	Post        bool
 	Data        map[string][]byte
 	Simple      bool `default:"false"`
+	Labels      map[string]string
 }
 
 type CertAuthority struct {
-	Name string
+	Name   string
+	Labels map[string]string
 }
 
 type CredentialHandler interface {
diff --git a/client/connector_create.go b/client/connector_create.go
index 1382211a..fde22f53 100644
--- a/client/connector_create.go
+++ b/client/connector_create.go
@@ -29,7 +29,13 @@ func (cli *VanClient) ConnectorCreateFromFile(ctx context.Context, secretFile st
 		fmt.Println("Could not read connection token", err.Error())
 		return nil, err
 	}
-	options.Yaml = yaml
+	ys := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme.Scheme,
+		scheme.Scheme)
+	options.Secret = &corev1.Secret{}
+	_, _, err = ys.Decode(yaml, nil, options.Secret)
+	if err != nil {
+		return nil, fmt.Errorf("Could not parse connection token: %w", err)
+	}
 	secret, err := cli.ConnectorCreateSecretFromData(ctx, options)
 	if err != nil {
 		return nil, err
@@ -48,84 +54,81 @@ func (cli *VanClient) ConnectorCreateFromFile(ctx context.Context, secretFile st
 func (cli *VanClient) ConnectorCreateSecretFromData(ctx context.Context, options types.ConnectorCreateOptions) (*corev1.Secret, error) {
 	current, err := kube.GetDeployment(types.TransportDeploymentName, options.SkupperNamespace, cli.KubeClient)
 	if err == nil {
-		s := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme.Scheme,
-			scheme.Scheme)
-		var secret corev1.Secret
-		_, _, err = s.Decode(options.Yaml, nil, &secret)
-		if err != nil {
+		if options.Secret == nil {
 			return nil, fmt.Errorf("Could not parse connection token: %w", err)
-		} else {
-			// Validating destination host
-			siteConfig, err := cli.SiteConfigInspect(context.Background(), nil)
-			if err != nil {
-				return nil, err
-			}
-			hostname := ""
-			if secret.ObjectMeta.Labels[types.SkupperTypeQualifier] == types.TypeToken {
-				if siteConfig.Spec.RouterMode == string(types.TransportModeEdge) {
-					hostname = secret.ObjectMeta.Annotations["edge-host"]
-				} else {
-					hostname = secret.ObjectMeta.Annotations["inter-router-host"]
-				}
-			} else {
-				destUrl, err := url.Parse(secret.ObjectMeta.Annotations[types.ClaimUrlAnnotationKey])
-				if err != nil {
-					return nil, fmt.Errorf("Invalid URL defined in token: %s", err)
-				}
-				hostname = destUrl.Hostname()
-			}
-			policy := NewClusterPolicyValidator(cli)
-			res := policy.ValidateOutgoingLink(hostname)
-			if !res.Allowed() {
-				return nil, fmt.Errorf("outgoing link to %s is not allowed", hostname)
-			}
+		}
+		var secret = options.Secret
 
-			cfg, err := cli.getRouterConfig(ctx, cli.Namespace)
-			if err != nil {
-				return nil, fmt.Errorf("error reading router config: %w", err)
-			}
-			linkHandler := domainkube.NewLinkHandlerKube(options.SkupperNamespace, siteConfig, cfg, cli.KubeClient, cli.RestConfig)
-			if options.Name == "" {
-				options.Name = domain.GenerateLinkName(linkHandler)
-			}
-			secret.ObjectMeta.Name = options.Name
-			err = domain.VerifyToken(&secret)
-			if err != nil {
-				return nil, err
+		// Validating destination host
+		siteConfig, err := cli.SiteConfigInspect(context.Background(), nil)
+		if err != nil {
+			return nil, err
+		}
+		hostname := ""
+		if secret.ObjectMeta.Labels[types.SkupperTypeQualifier] == types.TypeToken {
+			if siteConfig.Spec.RouterMode == string(types.TransportModeEdge) {
+				hostname = secret.ObjectMeta.Annotations["edge-host"]
+			} else {
+				hostname = secret.ObjectMeta.Annotations["inter-router-host"]
 			}
-			// Verify if site link can be created
-			err = domain.VerifyNotSelfOrDuplicate(secret, siteConfig.Reference.UID, linkHandler)
+		} else {
+			destUrl, err := url.Parse(secret.ObjectMeta.Annotations[types.ClaimUrlAnnotationKey])
 			if err != nil {
-				return nil, err
+				return nil, fmt.Errorf("Invalid URL defined in token: %s", err)
 			}
-			err = cli.VerifySecretCompatibility(secret)
+			hostname = destUrl.Hostname()
+		}
+		policy := NewClusterPolicyValidator(cli)
+		res := policy.ValidateOutgoingLink(hostname)
+		if !res.Allowed() {
+			return nil, fmt.Errorf("outgoing link to %s is not allowed", hostname)
+		}
+
+		cfg, err := cli.getRouterConfig(ctx, cli.Namespace)
+		if err != nil {
+			return nil, fmt.Errorf("error reading router config: %w", err)
+		}
+		linkHandler := domainkube.NewLinkHandlerKube(options.SkupperNamespace, siteConfig, cfg, cli.KubeClient, cli.RestConfig)
+		if options.Name == "" {
+			options.Name = domain.GenerateLinkName(linkHandler)
+		}
+		secret.ObjectMeta.Name = options.Name
+		err = domain.VerifyToken(secret)
+		if err != nil {
+			return nil, err
+		}
+		// Verify if site link can be created
+		err = domain.VerifyNotSelfOrDuplicate(*secret, siteConfig.Reference.UID, linkHandler)
+		if err != nil {
+			return nil, err
+		}
+		err = cli.VerifySecretCompatibility(*secret)
+		if err != nil {
+			return nil, err
+		}
+		if secret.ObjectMeta.Labels[types.SkupperTypeQualifier] == types.TypeClaimRequest {
+			// can site handle claims?
+			err := cli.requireSiteVersion(ctx, options.SkupperNamespace, "0.7.0")
 			if err != nil {
-				return nil, err
-			}
-			if secret.ObjectMeta.Labels[types.SkupperTypeQualifier] == types.TypeClaimRequest {
-				// can site handle claims?
-				err := cli.requireSiteVersion(ctx, options.SkupperNamespace, "0.7.0")
-				if err != nil {
-					return nil, fmt.Errorf("Claims not supported. %s", err)
-				}
-			}
-			secret.ObjectMeta.SetOwnerReferences([]metav1.OwnerReference{
-				kube.GetDeploymentOwnerReference(current),
-			})
-			if options.Cost != 0 {
-				if secret.ObjectMeta.Annotations == nil {
-					secret.ObjectMeta.Annotations = map[string]string{}
-				}
-				secret.ObjectMeta.Annotations[types.TokenCost] = strconv.Itoa(int(options.Cost))
+				return nil, fmt.Errorf("Claims not supported. %s", err)
 			}
-			_, err = cli.KubeClient.CoreV1().Secrets(options.SkupperNamespace).Create(context.TODO(), &secret, metav1.CreateOptions{})
-			if err == nil {
-				return &secret, nil
-			} else if errors.IsAlreadyExists(err) {
-				return &secret, fmt.Errorf("The connector secret \"%s\"already exists, please choose a different name", secret.ObjectMeta.Name)
-			} else {
-				return nil, fmt.Errorf("Failed to create connector secret: %w", err)
+		}
+		secret.ObjectMeta.SetOwnerReferences([]metav1.OwnerReference{
+			kube.GetDeploymentOwnerReference(current),
+		})
+		if options.Cost != 0 {
+			if secret.ObjectMeta.Annotations == nil {
+				secret.ObjectMeta.Annotations = map[string]string{}
 			}
+			secret.ObjectMeta.Annotations[types.TokenCost] = strconv.Itoa(int(options.Cost))
+		}
+		_, err = cli.KubeClient.CoreV1().Secrets(options.SkupperNamespace).Create(context.TODO(), secret, metav1.CreateOptions{})
+		if err == nil {
+			return secret, nil
+		} else if errors.IsAlreadyExists(err) {
+			return secret, fmt.Errorf("The connector secret \"%s\" already exists, please choose a different name", secret.ObjectMeta.Name)
+		} else {
+			return nil, fmt.Errorf("Failed to create connector secret: %w", err)
 		}
 	} else {
 		return nil, fmt.Errorf("Failed to retrieve router deployment: %w", err)
diff --git a/client/container/client.go b/client/container/client.go
index b0dcb56c..334f8aae 100644
--- a/client/container/client.go
+++ b/client/container/client.go
@@ -21,6 +21,8 @@ type Client interface {
 	ContainerList() ([]*Container, error)
 	ContainerInspect(id string) (*Container, error)
 	ContainerCreate(container *Container) error
+	ContainerUpdate(name string, fn func(newContainer *Container)) (*Container, error)
+	ContainerRename(currentName, newName string) error
 	ContainerRemove(id string) error
 	ContainerExec(id string, command []string) (string, error)
 	ContainerLogs(id string) (string, error)
diff --git a/client/podman/container.go b/client/podman/container.go
index c881b68f..957d21a6 100644
--- a/client/podman/container.go
+++ b/client/podman/container.go
@@ -8,6 +8,7 @@ import (
 	"time"
 
 	"github.com/go-openapi/runtime"
+	runtimeclient "github.com/go-openapi/runtime/client"
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/client/container"
 	"github.com/skupperproject/skupper/client/generated/libpod/client/containers"
@@ -132,6 +133,140 @@ func (p *PodmanRestClient) ContainerCreate(container *container.Container) error
 	return nil
 }
 
+// ContainerUpdate replaces the container (by name) with an identical copy
+// with an applied Customization (required).
+// To achieve that, it follows this procedure:
+// - Create a new container
+// - Stop current container
+// - Rename current container
+// - Rename the new container (replacing the original one)
+// - Starts the new container
+// - Removes the original container
+//
+// In case of failures during this process, the original container is restored.
+func (p *PodmanRestClient) ContainerUpdate(name string, fn func(newContainer *container.Container)) (*container.Container, error) {
+	if fn == nil {
+		return nil, fmt.Errorf("at least one customization is needed")
+	}
+
+	datetime := time.Now().Format("20060102150405")
+	c, err := p.ContainerInspect(name)
+	if err != nil {
+		return nil, err
+	}
+
+	newContainerName := fmt.Sprintf("%s-new-%s", c.Name, datetime)
+	cc := &container.Container{
+		Name:          newContainerName,
+		Image:         c.Image,
+		Env:           c.Env,
+		Labels:        c.Labels,
+		Annotations:   c.Annotations,
+		Networks:      c.Networks,
+		Mounts:        c.Mounts,
+		FileMounts:    c.FileMounts,
+		Ports:         c.Ports,
+		EntryPoint:    c.EntryPoint,
+		Command:       c.Command,
+		RestartPolicy: c.RestartPolicy,
+	}
+
+	// apply new container customization
+	fn(cc)
+	if cc.Name != newContainerName {
+		return nil, fmt.Errorf("container name cannot be changed")
+	}
+
+	// creating a new container
+	err = p.ContainerCreate(cc)
+	if err != nil {
+		return cc, err
+	}
+
+	// rollback this one in case of failures below
+	defer func() {
+		if err != nil {
+			_ = p.ContainerStop(cc.Name)
+			_ = p.ContainerRemove(cc.Name)
+		}
+	}()
+
+	// stopping current container
+	err = p.ContainerStop(name)
+	if err != nil {
+		return cc, err
+	}
+
+	// restarting current container
+	defer func() {
+		if err != nil {
+			_ = p.ContainerStart(name)
+		}
+	}()
+
+	// renaming current container to a backup name
+	backupName := fmt.Sprintf("%s-%s", name, datetime)
+	err = p.ContainerRename(name, backupName)
+	if err != nil {
+		return cc, err
+	}
+
+	// restoring original container
+	defer func() {
+		if err != nil {
+			_ = p.ContainerRename(backupName, name)
+		}
+	}()
+
+	// renaming new container to current name
+	err = p.ContainerRename(cc.Name, name)
+	if err != nil {
+		return cc, err
+	}
+
+	defer func() {
+		if err != nil {
+			_ = p.ContainerRename(name, newContainerName)
+		}
+	}()
+
+	// starting new container
+	err = p.ContainerStart(name)
+	if err != nil {
+		return cc, err
+	} else {
+		if removeErr := p.ContainerRemove(backupName); removeErr != nil {
+			fmt.Printf("Unable to remove backup container: %s - %s", backupName, err)
+			fmt.Println()
+		}
+	}
+
+	return cc, nil
+}
+
+// ContainerUpdateImage updates the image used by the given container (name).
+func (p *PodmanRestClient) ContainerUpdateImage(name string, newImage string) (*container.Container, error) {
+	err := p.ImagePull(newImage)
+	if err != nil {
+		return nil, fmt.Errorf("error pulling image %q: %s", newImage, err)
+	}
+	return p.ContainerUpdate(name, func(newContainer *container.Container) {
+		newContainer.Image = newImage
+	})
+}
+
+func (p *PodmanRestClient) ContainerRename(currentName, newName string) error {
+	cli := containers.New(p.RestClient, formats)
+	params := containers.NewContainerRenameLibpodParams()
+	params.PathName = currentName
+	params.QueryName = newName
+	_, err := cli.ContainerRenameLibpod(params)
+	if err != nil {
+		return fmt.Errorf("error renaming container %s to %s: %v", currentName, newName, err)
+	}
+	return nil
+}
+
 func (p *PodmanRestClient) ContainerRemove(name string) error {
 	cli := containers.New(p.RestClient, formats)
 	params := containers.NewContainerDeleteLibpodParams()
@@ -229,7 +364,10 @@ func (p *PodmanRestClient) ContainerExec(id string, command []string) (string, e
 		Client:             params.HTTPClient,
 	}
 
-	p.RestClient.Consumers["*/*"] = &responseReaderBody{}
+	restClient, ok := p.RestClient.(*runtimeclient.Runtime)
+	if ok {
+		restClient.Consumers["*/*"] = &responseReaderBody{}
+	}
 	result, err = p.RestClient.Submit(startOp)
 	if err != nil {
 		return "", fmt.Errorf("error starting execution: %v", err)
diff --git a/client/podman/container_mock.go b/client/podman/container_mock.go
new file mode 100644
index 00000000..6ad4c2e8
--- /dev/null
+++ b/client/podman/container_mock.go
@@ -0,0 +1,476 @@
+package podman
+
+import (
+	"fmt"
+	"os"
+	"path"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/go-openapi/runtime"
+	"github.com/go-openapi/strfmt"
+	"github.com/google/uuid"
+	"github.com/skupperproject/skupper/client/container"
+	"github.com/skupperproject/skupper/client/generated/libpod/client/containers"
+	"github.com/skupperproject/skupper/client/generated/libpod/client/volumes"
+	"github.com/skupperproject/skupper/client/generated/libpod/models"
+)
+
+func NewPodmanClientMock(containers []*container.Container) *PodmanRestClient {
+	return &PodmanRestClient{
+		RestClient: &RestClientMock{
+			Containers: containers,
+		},
+		endpoint: "",
+	}
+}
+
+// RestClientMock maintains (in memory) a set of containers
+// and volumes that are manipulated through libpod client.
+// It also maintains a set of volume files (in disk) to mock
+// podman volumes.
+// The optional ErrorHook can be set by tests to modify the
+// behavior of certain operations before they are actually
+// performed by the mock implementation.
+type RestClientMock struct {
+	Containers   []*container.Container
+	volumes      map[string]*container.Volume
+	volumesFiles map[string]map[string]string
+	volumesDir   string
+	ErrorHook    func(operation *runtime.ClientOperation) error
+}
+
+func (r *RestClientMock) MockVolumeFiles(volumes map[string]*container.Volume, volumesFiles map[string]map[string]string) error {
+	var err error
+	r.volumesDir, err = os.MkdirTemp("", "skupper-mock-")
+	if err != nil {
+		return err
+	}
+	r.volumes = volumes
+	r.volumesFiles = volumesFiles
+
+	prepareVolumeDir := func(volumeName string, volume *container.Volume) error {
+		volumeDir := path.Join(r.volumesDir, volumeName)
+		if err = os.Mkdir(volumeDir, 0755); err != nil {
+			return err
+		}
+		volume.Source = volumeDir
+		return nil
+	}
+	// creating temp directory for volume
+	for volumeName, volume := range r.volumes {
+		if err = prepareVolumeDir(volumeName, volume); err != nil {
+			return err
+		}
+	}
+	// creating volume files
+	for volumeName, volumeFiles := range r.volumesFiles {
+		volume, ok := r.volumes[volumeName]
+		// if volume not defined earlier, create it
+		if !ok {
+			v := &container.Volume{
+				Name: volumeName,
+			}
+			r.volumes[volumeName] = v
+			if err = prepareVolumeDir(volumeName, v); err != nil {
+				return err
+			}
+
+		}
+		if _, err = volume.CreateFiles(volumeFiles, true); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func (r *RestClientMock) CleanupMockVolumeDir() error {
+	if r.volumesDir != "" {
+		return os.RemoveAll(r.volumesDir)
+	}
+	return nil
+}
+
+func (r *RestClientMock) Submit(operation *runtime.ClientOperation) (interface{}, error) {
+	var res interface{}
+	var err error = nil
+
+	switch operation.ID {
+	case "ContainerListLibpod":
+		res, err = r.HandleContainerList(operation, r.ErrorHook)
+	case "ContainerInspectLibpod":
+		res, err = r.HandleContainerInspect(operation, r.ErrorHook)
+	case "ContainerCreateLibpod":
+		res, err = r.HandleContainerCreate(operation, r.ErrorHook)
+	case "ContainerDeleteLibpod":
+		res, err = r.HandleContainerDelete(operation, r.ErrorHook)
+	case "ContainerRenameLibpod":
+		res, err = r.HandleContainerRename(operation, r.ErrorHook)
+	case "ContainerStartLibpod":
+		res, err = r.HandleContainerStart(operation, r.ErrorHook)
+	case "ContainerStopLibpod":
+		res, err = r.HandleContainerStop(operation, r.ErrorHook)
+	case "VolumeInspectLibpod":
+		res, err = r.HandleVolumeInspect(operation, r.ErrorHook)
+	case "VolumeListLibpod":
+		res, err = r.HandleVolumeList(operation, r.ErrorHook)
+	}
+	return res, err
+}
+
+func (r *RestClientMock) HandleContainerList(operation *runtime.ClientOperation, errorHook func(operation *runtime.ClientOperation) error) (*containers.ContainerListLibpodOK, error) {
+	res := &containers.ContainerListLibpodOK{}
+	if len(r.Containers) > 0 {
+		res.Payload = []*models.ListContainer{}
+		for _, c := range r.Containers {
+			state := "running"
+			if !c.Running {
+				state = "exited"
+			}
+			res.Payload = append(res.Payload, &models.ListContainer{
+				Command:   c.Command,
+				Created:   strfmt.DateTime(c.CreatedAt),
+				CreatedAt: c.CreatedAt.String(),
+				ExitCode:  int32(c.ExitCode),
+				Exited:    !c.ExitedAt.Equal(time.Time{}),
+				ExitedAt:  c.ExitedAt.Unix(),
+				ID:        c.ID,
+				Image:     c.Image,
+				ImageID:   c.Image,
+				Labels:    c.Labels,
+				Mounts: func() []string {
+					var mounts []string
+					for _, m := range c.Mounts {
+						mounts = append(mounts, m.Destination)
+					}
+					return mounts
+				}(),
+				Names: []string{c.Name},
+				Networks: func() []string {
+					var networks []string
+					for _, n := range c.Networks {
+						networks = append(networks, n.ID)
+					}
+					return networks
+				}(),
+				Pod: c.Pod,
+				Ports: func() []*models.PortMapping {
+					var pm []*models.PortMapping
+					for _, port := range c.Ports {
+						containerPort, _ := strconv.ParseUint(port.Target, 10, 16)
+						hostPort, _ := strconv.ParseUint(port.Host, 10, 16)
+						pm = append(pm, &models.PortMapping{
+							ContainerPort: uint16(containerPort),
+							HostIP:        port.HostIP,
+							HostPort:      uint16(hostPort),
+							Protocol:      port.Protocol,
+						})
+					}
+					return pm
+				}(),
+				StartedAt: c.StartedAt.Unix(),
+				State:     state,
+			})
+		}
+	}
+	if errorHook != nil {
+		return res, errorHook(operation)
+	}
+	return res, nil
+}
+
+func (r *RestClientMock) HandleContainerInspect(operation *runtime.ClientOperation, hook func(operation *runtime.ClientOperation) error) (*containers.ContainerInspectLibpodOK, error) {
+	res := &containers.ContainerInspectLibpodOK{}
+	if hook != nil {
+		if err := hook(operation); err != nil {
+			return res, err
+		}
+	}
+	params := operation.Params.(*containers.ContainerInspectLibpodParams)
+	var c *container.Container
+	for _, cc := range r.Containers {
+		if cc.Name == params.Name || cc.ID == params.Name {
+			c = cc
+			break
+		}
+	}
+	if c == nil {
+		return nil, fmt.Errorf("container not found")
+	}
+	res.Payload = &containers.ContainerInspectLibpodOKBody{
+		Config: &models.InspectContainerConfig{
+			Annotations: c.Annotations,
+			Cmd:         c.Command,
+			Entrypoint:  strings.Join(c.EntryPoint, " "),
+			Env:         c.EnvSlice(),
+			Image:       c.Image,
+			Labels:      c.Labels,
+			User:        ToSpecGenerator(c).User,
+		},
+		Created:   strfmt.DateTime(c.CreatedAt),
+		ID:        c.ID,
+		Image:     c.Image,
+		ImageName: c.Image,
+		Mounts: func() []*models.InspectMount {
+			var m []*models.InspectMount
+			for _, mount := range c.Mounts {
+				m = append(m, &models.InspectMount{
+					Name:        mount.Name,
+					Source:      mount.Source,
+					Destination: mount.Destination,
+					Type:        "volume",
+					Mode:        mount.Mode,
+					RW:          mount.RW,
+				})
+			}
+			for _, fm := range c.FileMounts {
+				m = append(m, &models.InspectMount{
+					Source:      fm.Source,
+					Destination: fm.Destination,
+					Options:     fm.Options,
+					Type:        "bind",
+				})
+			}
+			return m
+		}(),
+		Name: c.Name,
+		NetworkSettings: func() *models.InspectNetworkSettings {
+			if len(c.Networks) == 0 && len(c.Ports) == 0 {
+				return nil
+			}
+			settings := &models.InspectNetworkSettings{
+				Networks: map[string]models.InspectAdditionalNetwork{},
+				Ports:    map[string][]models.InspectHostPort{},
+			}
+			for k, v := range c.Networks {
+				settings.Networks[k] = models.InspectAdditionalNetwork{
+					NetworkID:   "",
+					IPAddress:   "",
+					IPPrefixLen: 0,
+					MacAddress:  "",
+					Gateway:     "",
+					Aliases:     v.Aliases,
+				}
+			}
+			for _, port := range c.Ports {
+				portKey := fmt.Sprintf("%s/%s", port.Target, port.Protocol)
+				settings.Ports[portKey] = []models.InspectHostPort{
+					{HostIP: port.HostIP, HostPort: port.Host},
+				}
+			}
+			return settings
+		}(),
+		Pod: c.Pod,
+		State: &models.InspectContainerState{
+			ExitCode:   int32(c.ExitCode),
+			FinishedAt: strfmt.DateTime(c.ExitedAt),
+			Running:    c.Running,
+			StartedAt:  strfmt.DateTime(c.StartedAt),
+		},
+	}
+	return res, nil
+}
+
+func (r *RestClientMock) HandleContainerCreate(operation *runtime.ClientOperation, hook func(operation *runtime.ClientOperation) error) (*containers.ContainerCreateLibpodCreated, error) {
+	res := &containers.ContainerCreateLibpodCreated{}
+	if hook != nil {
+		if err := hook(operation); err != nil {
+			return res, err
+		}
+	}
+	params := operation.Params.(*containers.ContainerCreateLibpodParams)
+	spec := params.Create
+
+	for _, c := range r.Containers {
+		if c.Name == spec.Name {
+			return res, fmt.Errorf("container already exists")
+		}
+	}
+	c := &container.Container{
+		ID:          strings.Replace(uuid.New().String(), "-", "", -1),
+		Name:        spec.Name,
+		Pod:         spec.Pod,
+		Image:       spec.Image,
+		Env:         spec.Env,
+		Labels:      spec.Labels,
+		Annotations: spec.Annotations,
+		Networks: func() map[string]container.ContainerNetworkInfo {
+			res := map[string]container.ContainerNetworkInfo{}
+			for name, perNetworkOpts := range spec.Networks {
+				res[name] = container.ContainerNetworkInfo{
+					Aliases: perNetworkOpts.Aliases,
+				}
+			}
+			return res
+		}(),
+		Mounts: func() []container.Volume {
+			var res []container.Volume
+			for _, v := range spec.Volumes {
+				res = append(res, container.Volume{
+					Name:        v.Name,
+					Destination: v.Dest,
+				})
+			}
+			return res
+		}(),
+		FileMounts: func() []container.FileMount {
+			var res []container.FileMount
+			for _, fm := range spec.Mounts {
+				res = append(res, container.FileMount{
+					Source:      fm.Source,
+					Destination: fm.Destination,
+					Options:     fm.Options,
+				})
+			}
+			return nil
+		}(),
+		Ports: func() []container.Port {
+			var res []container.Port
+			// Port mapping
+			for _, pm := range spec.PortMappings {
+				res = append(res, container.Port{
+					Host:     strconv.Itoa(int(pm.HostPort)),
+					HostIP:   pm.HostIP,
+					Target:   strconv.Itoa(int(pm.ContainerPort)),
+					Protocol: pm.Protocol,
+				})
+			}
+			return res
+		}(),
+		EntryPoint:    spec.Entrypoint,
+		Command:       spec.Command,
+		RestartPolicy: spec.RestartPolicy,
+		RestartCount:  int(spec.RestartRetries),
+		CreatedAt:     time.Now(),
+	}
+	r.Containers = append(r.Containers, c)
+	return res, nil
+}
+
+func (r *RestClientMock) HandleContainerRename(operation *runtime.ClientOperation, hook func(operation *runtime.ClientOperation) error) (*containers.ContainerRenameLibpodNoContent, error) {
+	res := &containers.ContainerRenameLibpodNoContent{}
+	if hook != nil {
+		if err := hook(operation); err != nil {
+			return res, err
+		}
+	}
+	params := operation.Params.(*containers.ContainerRenameLibpodParams)
+	curName := params.PathName
+	newName := params.QueryName
+
+	// current name must exist
+	var cc *container.Container
+
+	for _, c := range r.Containers {
+		if c.Name == newName {
+			return res, fmt.Errorf("a container named %q already exists", newName)
+		} else if c.Name == curName {
+			cc = c
+		}
+	}
+	cc.Name = newName
+	return res, nil
+}
+
+func (r *RestClientMock) HandleContainerStart(operation *runtime.ClientOperation, hook func(operation *runtime.ClientOperation) error) (interface{}, error) {
+	res := &containers.ContainerStartLibpodNoContent{}
+	if hook != nil {
+		if err := hook(operation); err != nil {
+			return res, err
+		}
+	}
+	params := operation.Params.(*containers.ContainerStartLibpodParams)
+	for _, c := range r.Containers {
+		if c.Name == params.Name || c.ID == params.Name {
+			if !c.Running {
+				c.Running = true
+				c.StartedAt = time.Now()
+				c.ExitCode = 0
+				c.ExitedAt = time.Time{}
+			}
+			return res, nil
+		}
+	}
+	return res, fmt.Errorf("no container with name or ID %q", params.Name)
+}
+
+func (r *RestClientMock) HandleContainerStop(operation *runtime.ClientOperation, hook func(operation *runtime.ClientOperation) error) (interface{}, error) {
+	var res interface{}
+	if hook != nil {
+		if err := hook(operation); err != nil {
+			return res, err
+		}
+	}
+	params := operation.Params.(*containers.ContainerStopLibpodParams)
+	for _, c := range r.Containers {
+		if c.Name == params.Name || c.ID == params.Name {
+			res = &containers.ContainerStopLibpodNoContent{}
+			if c.Running {
+				c.Running = false
+				c.ExitedAt = time.Now()
+				c.ExitCode = 0
+			}
+			return res, nil
+		}
+	}
+	res = &containers.ContainerStopLibpodNotFound{}
+	return res, fmt.Errorf("no container with name or ID %q", params.Name)
+}
+
+func (r *RestClientMock) HandleContainerDelete(operation *runtime.ClientOperation, hook func(operation *runtime.ClientOperation) error) (interface{}, error) {
+	var res interface{}
+	if hook != nil {
+		if err := hook(operation); err != nil {
+			res = &containers.ContainerDeleteLibpodNoContent{}
+			return res, err
+		}
+	}
+	params := operation.Params.(*containers.ContainerDeleteLibpodParams)
+	for i, c := range r.Containers {
+		if c.Name == params.Name || c.ID == params.Name {
+			r.Containers = append(r.Containers[:i], r.Containers[i+1:]...)
+			res = &containers.ContainerDeleteLibpodOK{}
+			return res, nil
+		}
+	}
+	res = &containers.ContainerDeleteLibpodNoContent{}
+	return res, fmt.Errorf("no container with name or ID %q", params.Name)
+}
+
+func (r *RestClientMock) HandleVolumeInspect(operation *runtime.ClientOperation, hook func(operation *runtime.ClientOperation) error) (interface{}, error) {
+	var res = &volumes.VolumeInspectLibpodOK{}
+	if hook != nil {
+		if err := hook(operation); err != nil {
+			return res, err
+		}
+	}
+	params := operation.Params.(*volumes.VolumeInspectLibpodParams)
+	v, ok := r.volumes[params.Name]
+	if !ok {
+		return res, fmt.Errorf("no volume with name %q", params.Name)
+	}
+	res.Payload = &volumes.VolumeInspectLibpodOKBody{
+		Labels:     v.Labels,
+		Mountpoint: v.Source,
+		Name:       v.Name,
+	}
+	return res, nil
+}
+
+func (r *RestClientMock) HandleVolumeList(operation *runtime.ClientOperation, hook func(operation *runtime.ClientOperation) error) (interface{}, error) {
+	var res = &volumes.VolumeListLibpodOK{}
+	if hook != nil {
+		if err := hook(operation); err != nil {
+			return res, err
+		}
+	}
+	for vName, v := range r.volumes {
+		res.Payload = append(res.Payload, &models.VolumeConfigResponse{
+			Labels:     v.Labels,
+			Mountpoint: v.Source,
+			Name:       vName,
+		})
+	}
+	return res, nil
+}
diff --git a/client/podman/image.go b/client/podman/image.go
index 9683b4eb..59ea9450 100644
--- a/client/podman/image.go
+++ b/client/podman/image.go
@@ -1,7 +1,10 @@
 package podman
 
 import (
+	"encoding/base64"
+	"encoding/json"
 	"fmt"
+	"os"
 	"strings"
 
 	"github.com/go-openapi/runtime"
@@ -67,6 +70,7 @@ func (p *PodmanRestClient) ImagePull(id string) error {
 	params.OS = stringP("")
 	params.Variant = stringP("")
 	params.Policy = stringP("always")
+	params.XRegistryAuth = getXRegistryAuth(id)
 
 	// Need to do that as the default response reader is being closed too soon
 	op := &runtime.ClientOperation{
@@ -81,9 +85,79 @@ func (p *PodmanRestClient) ImagePull(id string) error {
 		Context:            params.Context,
 		Client:             params.HTTPClient,
 	}
-	_, err := p.RestClient.Submit(op)
+	res, err := p.RestClient.Submit(op)
 	if err != nil {
 		return fmt.Errorf("error pulling image %s: %v", id, err)
 	}
+	// eventually err is nil but auth problems are reported as json after a string msg
+	if resStr, ok := res.(string); ok {
+		resStrClean := strings.TrimLeftFunc(resStr, func(r rune) bool {
+			if r == '{' {
+				return false
+			}
+			return true
+		})
+		var jsonRes map[string]interface{}
+		if err = json.Unmarshal([]byte(resStrClean), &jsonRes); err == nil {
+			if errMsg, ok := jsonRes["error"]; ok && errMsg != "" {
+				return fmt.Errorf("unable to pull image %s: %v", id, errMsg)
+			}
+		}
+	}
+	return nil
+}
+
+func getXRegistryAuth(image string) *string {
+	authFile := os.Getenv("REGISTRY_AUTH_FILE")
+	// use the default
+	if authFile == "" {
+		return nil
+	}
+	data, err := os.ReadFile(authFile)
+	if err != nil {
+		fmt.Printf("Unable to read REGISTRY_AUTH_FILE - %s", err)
+		fmt.Println()
+		return nil
+	}
+	var jsonData map[string]interface{}
+	if err = json.Unmarshal(data, &jsonData); err != nil {
+		fmt.Printf("Unable to parse REGISTRY_AUTH_FILE - %s", err)
+		fmt.Println()
+		return nil
+	}
+	if auths, ok := jsonData["auths"]; ok {
+		authsMap, ok := auths.(map[string]interface{})
+		if !ok {
+			return nil
+		}
+		imageServer := strings.Split(image, "/")[0]
+		if serverData, ok := authsMap[imageServer]; ok {
+			if serverDataMap, ok := serverData.(map[string]interface{}); ok {
+				authInfo, ok := serverDataMap["auth"]
+				if !ok {
+					return nil
+				}
+				authInfoStr := authInfo.(string)
+				credentialsBytes, err := base64.StdEncoding.DecodeString(authInfoStr)
+				if err != nil {
+					fmt.Printf("Unable to decode base64 auth info for %s - %s", imageServer, err)
+					fmt.Println()
+					return nil
+				}
+				credentials := strings.Split(string(credentialsBytes), ":")
+				if len(credentials) != 2 {
+					return nil
+				}
+				registryAuthMap := map[string]map[string]string{}
+				registryAuthMap[imageServer] = map[string]string{
+					"username": credentials[0],
+					"password": credentials[1],
+				}
+				registryAuthMapJson, _ := json.Marshal(registryAuthMap)
+				registryAuthMapB64 := base64.StdEncoding.EncodeToString(registryAuthMapJson)
+				return &registryAuthMapB64
+			}
+		}
+	}
 	return nil
 }
diff --git a/client/podman/rest.go b/client/podman/rest.go
index 0395d093..b79a412e 100644
--- a/client/podman/rest.go
+++ b/client/podman/rest.go
@@ -14,8 +14,8 @@ import (
 	"strings"
 	"time"
 
-	runtime2 "github.com/go-openapi/runtime"
-	runtime "github.com/go-openapi/runtime/client"
+	"github.com/go-openapi/runtime"
+	runtimeclient "github.com/go-openapi/runtime/client"
 	"github.com/go-openapi/strfmt"
 	"github.com/skupperproject/skupper/client/generated/libpod/models"
 	"github.com/skupperproject/skupper/pkg/config"
@@ -34,7 +34,7 @@ var (
 )
 
 type PodmanRestClient struct {
-	RestClient *runtime.Runtime
+	RestClient runtime.ClientTransport
 	endpoint   string
 }
 
@@ -93,7 +93,7 @@ func NewPodmanClient(endpoint, basePath string) (*PodmanRestClient, error) {
 	if basePath == "" {
 		basePath = DEFAULT_BASE_PATH
 	}
-	c := runtime.New(hostPort, basePath, []string{u.Scheme})
+	c := runtimeclient.New(hostPort, basePath, []string{u.Scheme})
 	// Initializing transport like the http.DefaultTransport
 	// to avoid modifying it directly, as Runtime.Transport is
 	// set to http.DefaultTransport (variable)
@@ -170,7 +170,7 @@ func stringP(val string) *string {
 type responseReaderID struct {
 }
 
-func (r *responseReaderID) ReadResponse(response runtime2.ClientResponse, consumer runtime2.Consumer) (interface{}, error) {
+func (r *responseReaderID) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
 	switch response.Code() {
 	case 200, 201:
 		resp := &models.IDResponse{}
@@ -225,7 +225,7 @@ func (r *responseReaderBody) Consume(reader io.Reader, i interface{}) error {
 	return nil
 }
 
-func (r *responseReaderBody) ReadResponse(response runtime2.ClientResponse, consumer runtime2.Consumer) (interface{}, error) {
+func (r *responseReaderBody) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
 	switch response.Code() {
 	case 200, 201:
 		bodyStr := ""
@@ -245,7 +245,7 @@ func (r *responseReaderBody) ReadResponse(response runtime2.ClientResponse, cons
 	}
 }
 
-func (p *PodmanRestClient) ResponseIDReader(httpClient *runtime2.ClientOperation) {
+func (p *PodmanRestClient) ResponseIDReader(httpClient *runtime.ClientOperation) {
 	httpClient.Reader = &responseReaderID{}
 }
 
diff --git a/client/policy_validator.go b/client/policy_validator.go
index dd5e60e6..3b24ff72 100644
--- a/client/policy_validator.go
+++ b/client/policy_validator.go
@@ -130,11 +130,13 @@ func (p *ClusterPolicyValidator) AppliesToNS(policyName string) bool {
 	skupperPolicy, err := p.getSkupperPolicy()
 	// If unable to determine, revalidate
 	if err != nil {
+		recordPolicyValidatorEvent("unable to get policy client instance: %s - revalidating all policies", err)
 		return true
 	}
 	pol, err := skupperPolicy.Get(context.TODO(), policyName, v1.GetOptions{})
 	// If policy not found, revalidate
 	if err != nil {
+		recordPolicyValidatorEvent("unable to get policy %s: %s - revalidating all policies", policyName, err)
 		return true
 	}
 	namespace, _ := p.cli.KubeClient.CoreV1().Namespaces().Get(context.TODO(), p.cli.Namespace, v1.GetOptions{})
@@ -421,9 +423,7 @@ func (p *PolicyAPIClient) execGet(args ...string) (*PolicyAPIResult, error) {
 		} else if err != notEnabledErr {
 			err = fmt.Errorf("Unable to communicate with the API: %v", err)
 		}
-		if event.DefaultStore != nil {
-			event.Recordf("PolicyAPIError", err.Error())
-		}
+		recordPolicyValidatorEvent(err.Error())
 		return &PolicyAPIResult{
 			Allowed: false,
 			Enabled: false,
@@ -494,3 +494,9 @@ func (p *PolicyAPIClient) IncomingLink() (*PolicyAPIResult, error) {
 func (p *PolicyAPIClient) OutgoingLink(hostname string) (*PolicyAPIResult, error) {
 	return p.execGet("outgoinglink", hostname)
 }
+
+func recordPolicyValidatorEvent(format string, args ...interface{}) {
+	if event.DefaultStore != nil {
+		event.Recordf("PolicyAPIError", format, args...)
+	}
+}
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..106e14d2 100644
--- a/client/router_create.go
+++ b/client/router_create.go
@@ -1,19 +1,18 @@
 package client
 
 import (
-	"bytes"
 	"context"
 	"fmt"
 	"log"
+	"net"
 	"strconv"
 	"strings"
-	"text/template"
 	"time"
 
 	routev1 "github.com/openshift/api/route/v1"
+	"github.com/skupperproject/skupper/pkg/config"
 	"github.com/skupperproject/skupper/pkg/images"
 	"github.com/skupperproject/skupper/pkg/version"
-	"golang.org/x/crypto/bcrypt"
 	appsv1 "k8s.io/api/apps/v1"
 	corev1 "k8s.io/api/core/v1"
 	rbacv1 "k8s.io/api/rbac/v1"
@@ -22,6 +21,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"
@@ -101,10 +101,6 @@ func removeRules(original []rbacv1.PolicyRule, apigroups []string) []rbacv1.Poli
 	return rules
 }
 
-func hashPrometheusPassword(password string) ([]byte, error) {
-	return bcrypt.GenerateFromPassword([]byte(password), 14)
-}
-
 func (cli *VanClient) GetVanPrometheusServerSpec(options types.SiteConfigSpec, van *types.RouterSpec) {
 	// prometheus-server container index
 	const (
@@ -161,6 +157,7 @@ func (cli *VanClient) GetVanPrometheusServerSpec(options types.SiteConfigSpec, v
 		ObjectMeta: metav1.ObjectMeta{
 			Name:        types.PrometheusServiceAccountName,
 			Annotations: annotation,
+			Labels:      options.Labels,
 		},
 	})
 	van.PrometheusServer.ServiceAccounts = serviceAccounts
@@ -174,7 +171,8 @@ func (cli *VanClient) GetVanPrometheusServerSpec(options types.SiteConfigSpec, v
 			Kind:       "Role",
 		},
 		ObjectMeta: metav1.ObjectMeta{
-			Name: types.PrometheusRoleName,
+			Name:   types.PrometheusRoleName,
+			Labels: options.Labels,
 		},
 		Rules: cli.getControllerRules(),
 	})
@@ -187,7 +185,8 @@ func (cli *VanClient) GetVanPrometheusServerSpec(options types.SiteConfigSpec, v
 			Kind:       "RoleBinding",
 		},
 		ObjectMeta: metav1.ObjectMeta{
-			Name: types.PrometheusRoleBindingName,
+			Name:   types.PrometheusRoleBindingName,
+			Labels: options.Labels,
 		},
 		Subjects: []rbacv1.Subject{{
 			Kind: "ServiceAccount",
@@ -221,6 +220,7 @@ func (cli *VanClient) GetVanPrometheusServerSpec(options types.SiteConfigSpec, v
 			ObjectMeta: metav1.ObjectMeta{
 				Name:        types.PrometheusServiceName,
 				Annotations: annotations,
+				Labels:      options.Labels,
 			},
 			Spec: corev1.ServiceSpec{
 				Selector: van.PrometheusServer.Labels,
@@ -371,6 +371,7 @@ func (cli *VanClient) GetVanControllerSpec(options types.SiteConfigSpec, van *ty
 		ObjectMeta: metav1.ObjectMeta{
 			Name:        types.ControllerServiceAccountName,
 			Annotations: annotation,
+			Labels:      options.Labels,
 		},
 	})
 	van.Controller.ServiceAccounts = serviceAccounts
@@ -382,7 +383,8 @@ func (cli *VanClient) GetVanControllerSpec(options types.SiteConfigSpec, van *ty
 			Kind:       "Role",
 		},
 		ObjectMeta: metav1.ObjectMeta{
-			Name: types.ControllerRoleName,
+			Name:   types.ControllerRoleName,
+			Labels: options.Labels,
 		},
 		Rules: cli.getControllerRules(),
 	})
@@ -395,7 +397,8 @@ func (cli *VanClient) GetVanControllerSpec(options types.SiteConfigSpec, van *ty
 			Kind:       "RoleBinding",
 		},
 		ObjectMeta: metav1.ObjectMeta{
-			Name: types.ControllerRoleBindingName,
+			Name:   types.ControllerRoleBindingName,
+			Labels: options.Labels,
 		},
 		Subjects: []rbacv1.Subject{{
 			Kind: "ServiceAccount",
@@ -448,7 +451,8 @@ func (cli *VanClient) GetVanControllerSpec(options types.SiteConfigSpec, van *ty
 					Kind:       "Route",
 				},
 				ObjectMeta: metav1.ObjectMeta{
-					Name: types.ConsoleRouteName,
+					Name:   types.ConsoleRouteName,
+					Labels: options.Labels,
 				},
 				Spec: routev1.RouteSpec{
 					Path: "",
@@ -496,6 +500,7 @@ func (cli *VanClient) GetVanControllerSpec(options types.SiteConfigSpec, van *ty
 				Hosts:       controllerHosts,
 				ConnectJson: false,
 				Post:        post,
+				Labels:      options.Labels,
 			})
 		}
 		controllerPorts = append(controllerPorts, metricsPort)
@@ -510,6 +515,7 @@ func (cli *VanClient) GetVanControllerSpec(options types.SiteConfigSpec, van *ty
 				Hosts:       hosts,
 				ConnectJson: false,
 				Post:        false,
+				Labels:      options.Labels,
 			})
 		}
 		controllerPorts = append(controllerPorts, corev1.ServicePort{
@@ -534,6 +540,7 @@ func (cli *VanClient) GetVanControllerSpec(options types.SiteConfigSpec, van *ty
 			ObjectMeta: metav1.ObjectMeta{
 				Name:        types.ControllerServiceName,
 				Annotations: annotations,
+				Labels:      options.Labels,
 			},
 			Spec: corev1.ServiceSpec{
 				Selector: van.Controller.Labels,
@@ -716,7 +723,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)
 
@@ -805,7 +812,8 @@ func (cli *VanClient) GetRouterSpecFromOpts(options types.SiteConfigSpec, siteId
 			Kind:       "Role",
 		},
 		ObjectMeta: metav1.ObjectMeta{
-			Name: types.TransportRoleName,
+			Name:   types.TransportRoleName,
+			Labels: options.Labels,
 		},
 		Rules: cli.adjustRules(types.TransportPolicyRule),
 	})
@@ -818,7 +826,8 @@ func (cli *VanClient) GetRouterSpecFromOpts(options types.SiteConfigSpec, siteId
 			Kind:       "RoleBinding",
 		},
 		ObjectMeta: metav1.ObjectMeta{
-			Name: types.TransportRoleBindingName,
+			Name:   types.TransportRoleBindingName,
+			Labels: options.Labels,
 		},
 		Subjects: []rbacv1.Subject{{
 			Kind: "ServiceAccount",
@@ -846,21 +855,27 @@ func (cli *VanClient) GetRouterSpecFromOpts(options types.SiteConfigSpec, siteId
 		ObjectMeta: metav1.ObjectMeta{
 			Name:        types.TransportServiceAccountName,
 			Annotations: annotation,
+			Labels:      options.Labels,
 		},
 	})
 	van.Transport.ServiceAccounts = serviceAccounts
 
 	cas := []types.CertAuthority{}
 	cas = append(cas, types.CertAuthority{
-		Name: types.LocalCaSecret,
+		Name:   types.LocalCaSecret,
+		Labels: options.Labels,
 	})
 	if !isEdge {
 		cas = append(cas, types.CertAuthority{
-			Name: types.SiteCaSecret,
+			Name:   types.SiteCaSecret,
+			Labels: options.Labels,
 		})
 	}
 
-	cas = append(cas, types.CertAuthority{Name: types.ServiceCaSecret})
+	cas = append(cas, types.CertAuthority{
+		Name:   types.ServiceCaSecret,
+		Labels: options.Labels,
+	})
 
 	van.CertAuthoritys = cas
 
@@ -872,6 +887,7 @@ func (cli *VanClient) GetRouterSpecFromOpts(options types.SiteConfigSpec, siteId
 		Hosts:       []string{types.LocalTransportServiceName, types.LocalTransportServiceName + "." + van.Namespace + ".svc.cluster.local"},
 		ConnectJson: false,
 		Post:        false,
+		Labels:      options.Labels,
 	})
 	credentials = append(credentials, types.Credential{
 		CA:          types.LocalCaSecret,
@@ -880,6 +896,7 @@ func (cli *VanClient) GetRouterSpecFromOpts(options types.SiteConfigSpec, siteId
 		Hosts:       []string{},
 		ConnectJson: true,
 		Post:        false,
+		Labels:      options.Labels,
 	})
 
 	credentials = append(credentials, types.Credential{
@@ -889,6 +906,7 @@ func (cli *VanClient) GetRouterSpecFromOpts(options types.SiteConfigSpec, siteId
 		ConnectJson: false,
 		Post:        false,
 		Simple:      true,
+		Labels:      options.Labels,
 	})
 
 	if !isEdge {
@@ -899,6 +917,7 @@ func (cli *VanClient) GetRouterSpecFromOpts(options types.SiteConfigSpec, siteId
 			Hosts:       []string{types.TransportServiceName + "." + van.Namespace, types.TransportServiceName + "." + van.Namespace + ".svc.cluster.local"},
 			ConnectJson: false,
 			Post:        true,
+			Labels:      options.Labels,
 		})
 	}
 	if options.AuthMode == string(types.ConsoleAuthModeInternal) && (options.EnableFlowCollector || options.EnableRestAPI) {
@@ -913,6 +932,7 @@ func (cli *VanClient) GetRouterSpecFromOpts(options types.SiteConfigSpec, siteId
 			ConnectJson: false,
 			Data:        userData,
 			Post:        false,
+			Labels:      options.Labels,
 		})
 	}
 	van.TransportCredentials = credentials
@@ -927,6 +947,7 @@ func (cli *VanClient) GetRouterSpecFromOpts(options types.SiteConfigSpec, siteId
 		ObjectMeta: metav1.ObjectMeta{
 			Name:        types.LocalTransportServiceName,
 			Annotations: map[string]string{},
+			Labels:      options.Labels,
 		},
 		Spec: corev1.ServiceSpec{
 			Selector: van.Transport.Labels,
@@ -957,6 +978,7 @@ func (cli *VanClient) GetRouterSpecFromOpts(options types.SiteConfigSpec, siteId
 			ObjectMeta: metav1.ObjectMeta{
 				Name:        types.TransportServiceName,
 				Annotations: options.Router.ServiceAnnotations,
+				Labels:      options.Labels,
 			},
 			Spec: corev1.ServiceSpec{
 				Selector: van.Transport.Labels,
@@ -1010,6 +1032,7 @@ func (cli *VanClient) GetRouterSpecFromOpts(options types.SiteConfigSpec, siteId
 			ObjectMeta: metav1.ObjectMeta{
 				Name:        types.InterRouterRouteName,
 				Annotations: options.IngressAnnotations,
+				Labels:      options.Labels,
 			},
 			Spec: routev1.RouteSpec{
 				Path: "",
@@ -1035,6 +1058,7 @@ func (cli *VanClient) GetRouterSpecFromOpts(options types.SiteConfigSpec, siteId
 			ObjectMeta: metav1.ObjectMeta{
 				Name:        types.EdgeRouteName,
 				Annotations: options.IngressAnnotations,
+				Labels:      options.Labels,
 			},
 			Spec: routev1.RouteSpec{
 				Path: "",
@@ -1060,6 +1084,7 @@ func (cli *VanClient) GetRouterSpecFromOpts(options types.SiteConfigSpec, siteId
 			ObjectMeta: metav1.ObjectMeta{
 				Name:        types.ClaimRedemptionRouteName,
 				Annotations: options.IngressAnnotations,
+				Labels:      options.Labels,
 			},
 			Spec: routev1.RouteSpec{
 				Path: "",
@@ -1083,6 +1108,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 +1202,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
@@ -1129,7 +1216,7 @@ sasldb_path: /tmp/skrouterd.sasldb
 		saslData := &map[string]string{
 			"skrouterd.conf": config,
 		}
-		kube.NewConfigMap("skupper-sasl-config", saslData, nil, nil, siteOwnerRef, van.Namespace, cli.KubeClient)
+		kube.NewConfigMap("skupper-sasl-config", saslData, &options.Spec.Labels, nil, siteOwnerRef, van.Namespace, cli.KubeClient)
 	}
 	for _, sa := range van.Transport.ServiceAccounts {
 		sa.ObjectMeta.OwnerReferences = ownerRefs
@@ -1188,9 +1275,9 @@ sasldb_path: /tmp/skrouterd.sasldb
 		return err
 	}
 
-	kube.NewConfigMap(types.ServiceInterfaceConfigMap, nil, nil, nil, siteOwnerRef, van.Namespace, cli.KubeClient)
+	kube.NewConfigMap(types.ServiceInterfaceConfigMap, nil, &options.Spec.Labels, nil, siteOwnerRef, van.Namespace, cli.KubeClient)
 	initialConfig := qdr.AsConfigMapData(van.RouterConfig)
-	kube.NewConfigMap(types.TransportConfigMapName, &initialConfig, nil, nil, siteOwnerRef, van.Namespace, cli.KubeClient)
+	kube.NewConfigMap(types.TransportConfigMapName, &initialConfig, &options.Spec.Labels, nil, siteOwnerRef, van.Namespace, cli.KubeClient)
 
 	currentContext, cn := getCurrentContextOrDefault(ctx)
 	if cn != nil {
@@ -1497,7 +1584,7 @@ func (cli *VanClient) createIngress(site types.SiteConfig) error {
 		})
 	}
 
-	return kube.CreateIngress(types.IngressName, routes, site.Spec.IsIngressNginxIngress(), true, asOwnerReferences(site.Reference), namespace, site.Spec.IngressAnnotations, cli)
+	return kube.CreateIngress(types.IngressName, routes, site.Spec.IsIngressNginxIngress(), true, asOwnerReferences(site.Reference), namespace, site.Spec.IngressAnnotations, site.Spec.Labels, cli)
 }
 
 func (cli *VanClient) createContourProxies(site types.SiteConfig) error {
@@ -1535,7 +1622,7 @@ func (cli *VanClient) createContourProxies(site types.SiteConfig) error {
 			ServicePort: int(types.ClaimRedemptionPort),
 		})
 	}
-	return kube.CreateContourProxies(routes, asOwnerReference(site.Reference), cli.DynamicClient, namespace)
+	return kube.CreateContourProxies(routes, site.Spec.Labels, asOwnerReference(site.Reference), cli.DynamicClient, namespace)
 }
 
 func asOwnerReference(ref types.SiteConfigReference) *metav1.OwnerReference {
@@ -1579,69 +1666,8 @@ func asOwnerReferences(in types.SiteConfigReference) []metav1.OwnerReference {
 	return []metav1.OwnerReference{*ref}
 }
 
-type PrometheusInfo struct {
-	BasicAuth   bool
-	TlsAuth     bool
-	ServiceName string
-	Namespace   string
-	Port        string
-	User        string
-	Password    string
-	Hash        string
-}
-
-func scrapeConfigForPrometheus(info PrometheusInfo) string {
-	const prometheusConfig = `
-global:
-  scrape_interval:     15s
-  evaluation_interval: 15s
-alerting:
-  alertmanagers:
-  - static_configs:
-    - targets:
-rule_files:
-  # - "example-file.yml"
-scrape_configs:
-  - job_name: 'prometheus'
-    metrics_path: "/api/v1alpha1/metrics"
-    scheme: "https"
-    tls_config:
-      insecure_skip_verify: true
-    static_configs:
-    - targets: ["{{.ServiceName}}.{{.Namespace}}.svc.cluster.local:{{.Port}}"]
-`
-	var buf bytes.Buffer
-	promConfig := template.Must(template.New("promConfig").Parse(prometheusConfig))
-	promConfig.Execute(&buf, info)
-
-	return buf.String()
-}
-
-func webConfigForPrometheus(info PrometheusInfo) string {
-	const webConfig = `
-# TLS configuration.
-#{{- if .TlsAuth }}
-#tls_server_config:
-#  cert_file: /etc/tls/certs/tls.crt
-#  key_file: /etc/tls/certs/tls.key
-#{{- end}}
-#
-# Usernames and passwords required to connect to Prometheus.
-# Passwords are hashed with bcrypt: https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md#about-bcrypt
-#basic_auth_users:
-#{{- if .BasicAuth }}
-#  {{.User}}: {{.Hash}}
-#{{- end}}
-`
-	var buf bytes.Buffer
-	promConfig := template.Must(template.New("prmConfig").Parse(webConfig))
-	promConfig.Execute(&buf, info)
-
-	return buf.String()
-}
-
 func (cli *VanClient) createPrometheus(ctx context.Context, siteConfig *types.SiteConfig, van types.RouterSpec) error {
-	promInfo := PrometheusInfo{
+	promInfo := config.PrometheusInfo{
 		BasicAuth:   false,
 		TlsAuth:     false,
 		ServiceName: types.ControllerServiceName,
@@ -1659,14 +1685,14 @@ func (cli *VanClient) createPrometheus(ctx context.Context, siteConfig *types.Si
 		if siteConfig.Spec.PrometheusServer.Password != "" {
 			promInfo.Password = siteConfig.Spec.PrometheusServer.Password
 		}
-		hash, _ := hashPrometheusPassword(promInfo.Password)
+		hash, _ := config.HashPrometheusPassword(promInfo.Password)
 		promInfo.Hash = string(hash)
 	} else if siteConfig.Spec.PrometheusServer.AuthMode == string(types.PrometheusAuthModeTls) {
 		promInfo.TlsAuth = true
 	}
 	prometheusData := &map[string]string{
-		"prometheus.yml": scrapeConfigForPrometheus(promInfo),
-		"web-config.yml": webConfigForPrometheus(promInfo),
+		"prometheus.yml": config.ScrapeConfigForPrometheus(promInfo),
+		"web-config.yml": config.ScrapeWebConfigForPrometheus(promInfo),
 	}
 	siteOwnerRef := asOwnerReference(siteConfig.Reference)
 	var ownerRefs []metav1.OwnerReference
@@ -1674,7 +1700,7 @@ func (cli *VanClient) createPrometheus(ctx context.Context, siteConfig *types.Si
 		ownerRefs = []metav1.OwnerReference{*siteOwnerRef}
 	}
 
-	kube.NewConfigMap("prometheus-server-config", prometheusData, nil, nil, siteOwnerRef, van.Namespace, cli.KubeClient)
+	kube.NewConfigMap("prometheus-server-config", prometheusData, &siteConfig.Spec.Labels, nil, siteOwnerRef, van.Namespace, cli.KubeClient)
 
 	for _, sa := range van.PrometheusServer.ServiceAccounts {
 		sa.ObjectMeta.OwnerReferences = ownerRefs
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/flow-collector/handlers.go b/cmd/flow-collector/handlers.go
index 61138c49..0ebad711 100644
--- a/cmd/flow-collector/handlers.go
+++ b/cmd/flow-collector/handlers.go
@@ -180,10 +180,10 @@ func (c *Controller) promqueryHandler(w http.ResponseWriter, r *http.Request) {
 
 	w.Header().Set("Content-Type", "application/json")
 	proxyResp, err := client.Do(proxyReq)
-	w.WriteHeader(proxyResp.StatusCode)
 	if err != nil {
 		log.Printf("COLLECTOR: Prometheus query error: %s\n", err.Error())
 	} else {
+		w.WriteHeader(proxyResp.StatusCode)
 		data, _ := ioutil.ReadAll(proxyResp.Body)
 		proxyResp.Body.Close()
 		fmt.Fprintf(w, "%s\n", data)
diff --git a/cmd/flow-collector/main.go b/cmd/flow-collector/main.go
index 808026c9..610ec700 100644
--- a/cmd/flow-collector/main.go
+++ b/cmd/flow-collector/main.go
@@ -205,7 +205,7 @@ func main() {
 		}
 		flowRecordTtl, _ = time.ParseDuration(os.Getenv("FLOW_RECORD_TTL"))
 		enableConsole, _ = strconv.ParseBool(os.Getenv("ENABLE_CONSOLE"))
-		prometheusUrl = os.Getenv("PROMETHEUS_URL")
+		prometheusUrl = "http://skupper-prometheus:9090/api/v1/"
 	}
 
 	tlsConfig := certs.GetTlsConfigRetriever(true, types.ControllerConfigPath+"tls.crt", types.ControllerConfigPath+"tls.key", types.ControllerConfigPath+"ca.crt")
diff --git a/cmd/service-controller/links.go b/cmd/service-controller/links.go
index 4a44e293..47ef6ae4 100644
--- a/cmd/service-controller/links.go
+++ b/cmd/service-controller/links.go
@@ -12,6 +12,8 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	serializerjson "k8s.io/apimachinery/pkg/runtime/serializer/json"
+	"k8s.io/client-go/kubernetes/scheme"
 
 	"github.com/gorilla/mux"
 
@@ -171,7 +173,15 @@ func (m *LinkManager) deleteLink(name string) (bool, error) {
 }
 
 func (m *LinkManager) createLink(cost int, token []byte) error {
-	secret, err := m.cli.ConnectorCreateSecretFromData(context.Background(), types.ConnectorCreateOptions{Cost: int32(cost), SkupperNamespace: m.cli.Namespace, Yaml: token})
+	ys := serializerjson.NewYAMLSerializer(serializerjson.DefaultMetaFactory, scheme.Scheme,
+		scheme.Scheme)
+	var secret = &corev1.Secret{}
+	_, _, err := ys.Decode(token, nil, secret)
+	if err != nil {
+		return fmt.Errorf("Could not parse connection token: %w", err)
+	}
+
+	secret, err = m.cli.ConnectorCreateSecretFromData(context.Background(), types.ConnectorCreateOptions{Cost: int32(cost), SkupperNamespace: m.cli.Namespace, Secret: secret})
 	if err != nil {
 		return err
 	}
diff --git a/cmd/service-controller/policy_controller.go b/cmd/service-controller/policy_controller.go
index e17e8bab..f7f103c1 100644
--- a/cmd/service-controller/policy_controller.go
+++ b/cmd/service-controller/policy_controller.go
@@ -80,6 +80,11 @@ func (c *PolicyController) OnDelete(obj interface{}) {
 	c.enqueue(obj)
 }
 
+func (c *PolicyController) logRecord(msg string) {
+	log.Println(msg)
+	event.Record(c.name, msg)
+}
+
 func (c *PolicyController) start(stopCh <-chan struct{}) error {
 	go func() {
 		period := time.NewTicker(time.Second)
@@ -92,7 +97,7 @@ func (c *PolicyController) start(stopCh <-chan struct{}) error {
 			c.createInformer()
 			go c.informer.Run(crdCh)
 			if ok := cache.WaitForCacheSync(crdCh, c.informer.HasSynced); !ok {
-				event.Recordf(c.name, "Error waiting for cache to sync")
+				c.logRecord("Error waiting for cache to sync")
 				close(crdCh)
 				return false
 			}
@@ -101,9 +106,9 @@ func (c *PolicyController) start(stopCh <-chan struct{}) error {
 		}
 		for {
 			if !enabled && c.validator.Enabled() {
-				log.Println("Skupper policy is enabled")
+				c.logRecord("Skupper policy is enabled")
 				if !c.validator.HasPermission() {
-					log.Printf("-> No permission to read SkupperClusterPolicies")
+					c.logRecord("-> No permission to read SkupperClusterPolicies")
 				} else {
 					if informerRunning = startInformer(); !informerRunning {
 						continue
@@ -114,7 +119,7 @@ func (c *PolicyController) start(stopCh <-chan struct{}) error {
 				enabled = true
 			} else if !c.validator.Enabled() && !disabledReported {
 				disabledReported = true
-				log.Printf("Skupper policy is disabled")
+				c.logRecord("Skupper policy is disabled")
 			}
 
 			select {
@@ -123,20 +128,20 @@ func (c *PolicyController) start(stopCh <-chan struct{}) error {
 					if informerRunning {
 						close(crdCh)
 					}
-					log.Println("Skupper policy has been disabled")
+					c.logRecord("Skupper policy has been disabled")
 					// reverts what has been denied by policies
 					c.validateStateChanged()
 					enabled = false
 				} else if enabled && !informerRunning && c.validator.HasPermission() {
 					// permission has been granted, running informer
 					informerRunning = startInformer()
-					log.Println("Permission to read SkupperClusterPolicies has been granted")
+					c.logRecord("Permission to read SkupperClusterPolicies has been granted")
 				} else if enabled && informerRunning && !c.validator.HasPermission() {
 					// permission revoked, stopping informer
 					close(crdCh)
 					informerRunning = false
 					c.validateStateChanged()
-					log.Println("Permission to read SkupperClusterPolicies has been revoked")
+					c.logRecord("Permission to read SkupperClusterPolicies has been revoked")
 				}
 			case <-stopCh:
 				if enabled {
@@ -216,13 +221,6 @@ func (c *PolicyController) validateIncomingLinkStateChanged() {
 		return
 	}
 
-	// Changed to allowed
-	if allowed {
-		event.Recordf(c.name, "[%s] allowing links", source)
-	} else {
-		event.Recordf(c.name, "[%s] blocking links", source)
-	}
-
 	siteConfig, err := c.cli.SiteConfigInspect(context.Background(), nil)
 	if err != nil {
 		event.Recordf(c.name, "[%s] error retrieving site config: %v", source, err)
@@ -240,8 +238,10 @@ func (c *PolicyController) validateIncomingLinkStateChanged() {
 
 		// Changed to allowed
 		if allowed {
+			event.Recordf(c.name, "[%s] allowing links", source)
 			current.AddListener(listenerFn(siteConfig.Spec.Router))
 		} else {
+			event.Recordf(c.name, "[%s] blocking links", source)
 			delete(current.Listeners, listenerName)
 		}
 	}
@@ -380,7 +380,9 @@ func (c *PolicyController) validateServiceStateChanged() {
 		}
 		if !res.Allowed() {
 			err = c.cli.ServiceInterfaceRemove(context.Background(), service.Address)
-			if err != nil {
+			if err == nil {
+				event.Recordf(c.name, "[validateServiceStateChanged] removing service definition %s", service.Address)
+			} else if !strings.Contains(err.Error(), "not defined") {
 				event.Recordf(c.name, "[validateServiceStateChanged] error removing service definition %s: %v", service.Address, err)
 				return
 			}
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..fab16e2c 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 {
@@ -458,10 +460,25 @@ installation that can then be connected to other skupper installations`,
 	cmd.Flags().StringVarP(&routerCreateOpts.Ingress, "ingress", "", "", "Setup Skupper ingress to one of: ["+strings.Join(types.ValidIngressOptions(platform), "|")+"].")
 	cmd.Flags().StringVarP(&initFlags.routerMode, "router-mode", "", string(types.TransportModeInterior), "Skupper router-mode")
 
-	cmd.Flags().StringSliceVar(&initFlags.labels, "labels", []string{}, "Labels to add to skupper pods")
+	cmd.Flags().StringSliceVar(&initFlags.labels, "labels", []string{}, "Labels to add to resources created by skupper")
 	cmd.Flags().StringVarP(&routerLogging, "router-logging", "", "", "Logging settings for router. 'trace', 'debug', 'info' (default), 'notice', 'warning', and 'error' are valid values.")
 	cmd.Flags().StringVarP(&routerCreateOpts.Router.DebugMode, "router-debug-mode", "", "", "Enable debug mode for router ('asan' or 'gdb' are valid values)")
 
+	cmd.Flags().StringVarP(&routerCreateOpts.PrometheusServer.ExternalServer, "external-prometheus-server", "", "", "External prometheus server for metric aggregation. Valid only when --enable-flow-collector")
+	cmd.Flags().StringVarP(&routerCreateOpts.PrometheusServer.AuthMode, "prometheus-auth", "", "", "Authentication mode for skupper prometheus server. One of: 'tls', 'basic', 'unsecured'")
+	cmd.Flags().StringVarP(&routerCreateOpts.PrometheusServer.User, "prometheus-user", "", "", "Skupper prometheus user. Valid only when --prometheus-auth=basic")
+	cmd.Flags().StringVarP(&routerCreateOpts.PrometheusServer.Password, "prometheus-password", "", "", "Skupper prometheus user. Valid only when --prometheus-auth=basic")
+
+	// hide prometheus flags until external server supported
+	f := cmd.Flag("external-prometheus-server")
+	f.Hidden = true
+	f = cmd.Flag("prometheus-auth")
+	f.Hidden = true
+	f = cmd.Flag("prometheus-user")
+	f.Hidden = true
+	f = cmd.Flag("prometheus-password")
+	f.Hidden = true
+
 	cmd.Flags().SortFlags = false
 
 	// platform specific flags
@@ -498,7 +515,7 @@ func NewCmdUpdate(skupperCli SkupperSiteClient) *cobra.Command {
 		PreRun: skupperCli.NewClient,
 		RunE:   skupperCli.Update,
 	}
-	cmd.Flags().BoolVarP(&forceHup, "force-restart", "", false, "Restart skupper daemons even if image tag is not updated")
+	skupperCli.UpdateFlags(cmd)
 	return cmd
 }
 
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/cmd/skupper/skupper_kube_site.go b/cmd/skupper/skupper_kube_site.go
index 35aa343d..af6528a3 100644
--- a/cmd/skupper/skupper_kube_site.go
+++ b/cmd/skupper/skupper_kube_site.go
@@ -173,10 +173,6 @@ func (s *SkupperKubeSite) CreateFlags(cmd *cobra.Command) {
 	cmd.Flags().StringVar(&routerCreateOpts.FlowCollector.CpuLimit, "flow-collector-cpu-limit", "", "CPU limit for flow collector pods")
 	cmd.Flags().StringVar(&routerCreateOpts.FlowCollector.MemoryLimit, "flow-collector-memory-limit", "", "Memory limit for flow collector pods")
 
-	cmd.Flags().StringVarP(&routerCreateOpts.PrometheusServer.ExternalServer, "external-prometheus-server", "", "", "External prometheus server for metric aggregation. Valid only when --enable-flow-collector")
-	cmd.Flags().StringVarP(&routerCreateOpts.PrometheusServer.AuthMode, "prometheus-auth", "", "", "Authentication mode for skupper prometheus server. One of: 'tls', 'basic', 'unsecured'")
-	cmd.Flags().StringVarP(&routerCreateOpts.PrometheusServer.User, "prometheus-user", "", "", "Skupper prometheus user. Valid only when --prometheus-auth=basic")
-	cmd.Flags().StringVarP(&routerCreateOpts.PrometheusServer.Password, "prometheus-password", "", "", "Skupper prometheus user. Valid only when --prometheus-auth=basic")
 	cmd.Flags().StringVar(&routerCreateOpts.PrometheusServer.Cpu, "prometheus-cpu", "", "CPU request for prometheus pods")
 	cmd.Flags().StringVar(&routerCreateOpts.PrometheusServer.Memory, "prometheus-memory", "", "Memory request for prometheus pods")
 	cmd.Flags().StringVar(&routerCreateOpts.PrometheusServer.CpuLimit, "prometheus-cpu-limit", "", "CPU limit for prometheus pods")
@@ -192,15 +188,6 @@ func (s *SkupperKubeSite) CreateFlags(cmd *cobra.Command) {
 	f.Hidden = true
 	f = cmd.Flag("router-disable-mutual-tls")
 	f.Hidden = true
-	// hide prometheus flags until external server supported
-	f = cmd.Flag("external-prometheus-server")
-	f.Hidden = true
-	f = cmd.Flag("prometheus-auth")
-	f.Hidden = true
-	f = cmd.Flag("prometheus-user")
-	f.Hidden = true
-	f = cmd.Flag("prometheus-password")
-	f.Hidden = true
 }
 
 func (s *SkupperKubeSite) Delete(cmd *cobra.Command, args []string) error {
@@ -319,7 +306,9 @@ func (s *SkupperKubeSite) Update(cmd *cobra.Command, args []string) error {
 	return nil
 }
 
-func (s *SkupperKubeSite) UpdateFlags(cmd *cobra.Command) {}
+func (s *SkupperKubeSite) UpdateFlags(cmd *cobra.Command) {
+	cmd.Flags().BoolVarP(&forceHup, "force-restart", "", false, "Restart skupper daemons even if image tag is not updated")
+}
 
 func (s *SkupperKubeSite) Version(cmd *cobra.Command, args []string) error {
 	cli := s.kube.Cli
diff --git a/cmd/skupper/skupper_link.go b/cmd/skupper/skupper_link.go
index 7fd527dd..1b4d4eda 100644
--- a/cmd/skupper/skupper_link.go
+++ b/cmd/skupper/skupper_link.go
@@ -4,12 +4,15 @@ import (
 	"context"
 	"fmt"
 	"io/ioutil"
-
+	"strconv"
 	"time"
 
 	"github.com/skupperproject/skupper/pkg/utils"
 	"github.com/skupperproject/skupper/pkg/utils/formatter"
+	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/runtime/serializer/json"
+	"k8s.io/client-go/kubernetes/scheme"
 	_ "k8s.io/client-go/plugin/pkg/client/auth"
 
 	"github.com/spf13/cobra"
@@ -44,7 +47,23 @@ func NewCmdLinkCreate(skupperClient SkupperLinkClient, flag string) *cobra.Comma
 			if err != nil {
 				return fmt.Errorf("Could not read connection token: %s", err.Error())
 			}
-			connectorCreateOpts.Yaml = yaml
+			costFlag := cmd.Flag("cost")
+			ys := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme.Scheme,
+				scheme.Scheme)
+			var secret = &corev1.Secret{}
+			_, _, err = ys.Decode(yaml, nil, secret)
+			if err != nil {
+				return fmt.Errorf("Could not parse connection token: %w", err)
+			}
+			connectorCreateOpts.Secret = secret
+			if secret.ObjectMeta.Annotations != nil && !costFlag.Changed {
+				if costStr, ok := secret.ObjectMeta.Annotations[types.TokenCost]; ok {
+					if cost, err := strconv.Atoi(costStr); err == nil {
+						connectorCreateOpts.Cost = int32(cost)
+					}
+				}
+			}
+
 			return skupperClient.Create(cmd, args)
 		},
 	}
diff --git a/cmd/skupper/skupper_podman.go b/cmd/skupper/skupper_podman.go
index 40fd7d5f..3e799928 100644
--- a/cmd/skupper/skupper_podman.go
+++ b/cmd/skupper/skupper_podman.go
@@ -14,7 +14,7 @@ var notImplementedErr = fmt.Errorf("Not implemented")
 
 var SkupperPodmanCommands = []string{
 	"switch", "init", "delete", "status", "version", "token", "link",
-	"service", "expose", "unexpose", "revoke-access",
+	"service", "expose", "unexpose", "revoke-access", "update",
 }
 
 type SkupperPodman struct {
diff --git a/cmd/skupper/skupper_podman_link.go b/cmd/skupper/skupper_podman_link.go
index 5ac28484..1e5daef5 100644
--- a/cmd/skupper/skupper_podman_link.go
+++ b/cmd/skupper/skupper_podman_link.go
@@ -1,15 +1,10 @@
 package main
 
 import (
-	"fmt"
-
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/pkg/domain"
 	"github.com/skupperproject/skupper/pkg/domain/podman"
 	"github.com/spf13/cobra"
-	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/runtime/serializer/json"
-	"k8s.io/client-go/kubernetes/scheme"
 )
 
 type SkupperPodmanLink struct {
@@ -19,15 +14,8 @@ type SkupperPodmanLink struct {
 
 func (s *SkupperPodmanLink) Create(cmd *cobra.Command, args []string) error {
 	// reading secret from file
-	var secret corev1.Secret
-	serializer := json.NewSerializerWithOptions(json.DefaultMetaFactory, scheme.Scheme, scheme.Scheme, json.SerializerOptions{Yaml: true})
-	_, _, err := serializer.Decode(connectorCreateOpts.Yaml, nil, &secret)
-	if err != nil {
-		return fmt.Errorf("error decoding token - %w", err)
-	}
-
 	linkHandler := podman.NewLinkHandlerPodman(s.podman.currentSite, s.podman.cli)
-	return linkHandler.Create(&secret, connectorCreateOpts.Name, int(connectorCreateOpts.Cost))
+	return linkHandler.Create(connectorCreateOpts.Secret, connectorCreateOpts.Name, int(connectorCreateOpts.Cost))
 }
 
 func (s *SkupperPodmanLink) CreateFlags(cmd *cobra.Command) {}
diff --git a/cmd/skupper/skupper_podman_site.go b/cmd/skupper/skupper_podman_site.go
index 78b697a9..8f04da6c 100644
--- a/cmd/skupper/skupper_podman_site.go
+++ b/cmd/skupper/skupper_podman_site.go
@@ -9,6 +9,7 @@ import (
 	"github.com/skupperproject/skupper/client/container"
 	"github.com/skupperproject/skupper/pkg/domain"
 	podman "github.com/skupperproject/skupper/pkg/domain/podman"
+	"github.com/skupperproject/skupper/pkg/domain/podman/update"
 	"github.com/skupperproject/skupper/pkg/qdr"
 	"github.com/spf13/cobra"
 )
@@ -16,6 +17,7 @@ import (
 type SkupperPodmanSite struct {
 	podman *SkupperPodman
 	flags  PodmanInitFlags
+	up     *domain.UpdateProcessor
 }
 
 type PodmanInitFlags struct {
@@ -67,6 +69,7 @@ func (s *SkupperPodmanSite) Create(cmd *cobra.Command, args []string) error {
 		ConsolePassword:              routerCreateOpts.Password,
 		FlowCollectorRecordTtl:       routerCreateOpts.FlowCollector.FlowRecordTtl,
 		RouterOpts:                   routerCreateOpts.Router,
+		PrometheusOpts:               routerCreateOpts.PrometheusServer,
 	}
 
 	siteHandler, err := podman.NewSitePodmanHandler(site.PodmanEndpoint)
@@ -239,10 +242,22 @@ func (s *SkupperPodmanSite) Platform() types.Platform {
 }
 
 func (s *SkupperPodmanSite) Update(cmd *cobra.Command, args []string) error {
-	return notImplementedErr
+	/*
+	  Registering tasks to be analyzed against current site version
+	*/
+	// updates images for all skupper containers
+	s.up.RegisterTasks(update.NewContainerImagesTask(s.podman.cli))
+	// updates site version number (always the last one)
+	s.up.RegisterTasks(update.NewVersionUpdateTask(s.podman.cli))
+	// process eligible tasks
+	return s.up.Process(s.podman.currentSite.Version)
 }
 
-func (s *SkupperPodmanSite) UpdateFlags(cmd *cobra.Command) {}
+func (s *SkupperPodmanSite) UpdateFlags(cmd *cobra.Command) {
+	s.up = &domain.UpdateProcessor{}
+	cmd.Flags().BoolVar(&s.up.DryRun, "dry-run", false, "only prints the tasks to be performed, but does not run any action")
+	cmd.Flags().BoolVar(&s.up.Verbose, "verbose", false, "displays tasks and post tasks being executed")
+}
 
 func (s *SkupperPodmanSite) Version(cmd *cobra.Command, args []string) error {
 	site := s.podman.currentSite
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/config/prometheus-web-config.yml.template b/pkg/config/prometheus-web-config.yml.template
new file mode 100644
index 00000000..6c93e9e1
--- /dev/null
+++ b/pkg/config/prometheus-web-config.yml.template
@@ -0,0 +1,13 @@
+# TLS configuration.
+#{{- if .TlsAuth }}
+#tls_server_config:
+#  cert_file: /etc/tls/certs/tls.crt
+#  key_file: /etc/tls/certs/tls.key
+#{{- end}}
+#
+# Usernames and passwords required to connect to Prometheus.
+# Passwords are hashed with bcrypt: https://github.com/prometheus/exporter-toolkit/blob/master/docs/web-configuration.md#about-bcrypt
+#basic_auth_users:
+{{- if .BasicAuth}}
+#  {{.User}}: {{.Hash}}
+{{- end}}
diff --git a/pkg/config/prometheus.go b/pkg/config/prometheus.go
new file mode 100644
index 00000000..1d1bac87
--- /dev/null
+++ b/pkg/config/prometheus.go
@@ -0,0 +1,48 @@
+package config
+
+import (
+	"bytes"
+	_ "embed"
+	"text/template"
+
+	"golang.org/x/crypto/bcrypt"
+)
+
+var (
+	//go:embed prometheus.yml.template
+	PrometheusConfig string
+
+	//go:embed prometheus-web-config.yml.template
+	WebConfigForPrometheus string
+)
+
+type PrometheusInfo struct {
+	BasicAuth   bool
+	TlsAuth     bool
+	ServiceName string
+	Namespace   string
+	Port        string
+	User        string
+	Password    string
+	Hash        string
+}
+
+func ScrapeConfigForPrometheus(info PrometheusInfo) string {
+	var buf bytes.Buffer
+	promConfig := template.Must(template.New("promConfig").Parse(PrometheusConfig))
+	promConfig.Execute(&buf, info)
+
+	return buf.String()
+}
+
+func ScrapeWebConfigForPrometheus(info PrometheusInfo) string {
+	var buf bytes.Buffer
+	promConfig := template.Must(template.New("prmConfig").Parse(WebConfigForPrometheus))
+	promConfig.Execute(&buf, info)
+
+	return buf.String()
+}
+
+func HashPrometheusPassword(password string) ([]byte, error) {
+	return bcrypt.GenerateFromPassword([]byte(password), 14)
+}
diff --git a/pkg/config/prometheus.yml.template b/pkg/config/prometheus.yml.template
new file mode 100644
index 00000000..9d45e2f9
--- /dev/null
+++ b/pkg/config/prometheus.yml.template
@@ -0,0 +1,21 @@
+global:
+  scrape_interval:     15s
+  evaluation_interval: 15s
+alerting:
+  alertmanagers:
+    - static_configs:
+        - targets:
+rule_files:
+# - "example-file.yml"
+scrape_configs:
+  - job_name: 'prometheus'
+    metrics_path: "/api/v1alpha1/metrics"
+    scheme: "https"
+    tls_config:
+      insecure_skip_verify: true
+    static_configs:
+      {{- if .Namespace}}
+      - targets: ["{{.ServiceName}}.{{.Namespace}}.svc.cluster.local:{{.Port}}"]
+      {{- else}}
+      - targets: ["{{.ServiceName}}:{{.Port}}"]
+      {{- end}}
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/domain/podman/common.go b/pkg/domain/podman/common.go
index 8c04edfc..35e915c6 100644
--- a/pkg/domain/podman/common.go
+++ b/pkg/domain/podman/common.go
@@ -2,9 +2,6 @@ package podman
 
 import (
 	"fmt"
-	"os"
-	"os/user"
-	"strings"
 
 	"github.com/skupperproject/skupper/pkg/utils"
 
@@ -16,9 +13,9 @@ const (
 )
 
 var (
-	Username                = readUsername()
+	Username                = utils.ReadUsername()
 	SkupperContainerVolumes = []string{"skupper-services", "skupper-local-server", "skupper-internal", "skupper-site-server", SharedTlsCertificates,
-		types.ConsoleServerSecret, types.ConsoleUsersSecret}
+		types.ConsoleServerSecret, types.ConsoleUsersSecret, "prometheus-server-config", "prometheus-storage-volume"}
 )
 
 func OwnedBySkupper(resource string, labels map[string]string) error {
@@ -31,11 +28,3 @@ func OwnedBySkupper(resource string, labels map[string]string) error {
 	}
 	return nil
 }
-
-func readUsername() string {
-	u, err := user.Current()
-	if err != nil {
-		return utils.DefaultStr(os.Getenv("USER"), os.Getenv("USERNAME"))
-	}
-	return strings.Join(strings.Fields(u.Username), "")
-}
diff --git a/pkg/domain/podman/component.go b/pkg/domain/podman/component.go
index 0d721349..177594be 100644
--- a/pkg/domain/podman/component.go
+++ b/pkg/domain/podman/component.go
@@ -67,6 +67,13 @@ func (s *SkupperComponentHandler) Get(name string) (domain.SkupperComponent, err
 			Labels:        c.Labels,
 			SiteIngresses: siteIngresses,
 		}
+	case types.PrometheusDeploymentName:
+		component = &domain.Prometheus{
+			Image:         c.Image,
+			Env:           c.Env,
+			Labels:        c.Labels,
+			SiteIngresses: siteIngresses,
+		}
 	default:
 		return nil, fmt.Errorf("invalid component: %s", componentName)
 	}
diff --git a/pkg/domain/podman/controller/site_beacons.go b/pkg/domain/podman/controller/site_beacons.go
index d71ef713..12b99905 100644
--- a/pkg/domain/podman/controller/site_beacons.go
+++ b/pkg/domain/podman/controller/site_beacons.go
@@ -270,7 +270,8 @@ func (p *ContainerProcessInformer) containerToProcess(cc *container.Container) *
 	if ipAddress != "" {
 		pp.SourceHost = &ipAddress
 	}
-	if container.IsOwnedBySkupper(cc.Labels) {
+	_, isServiceContainer := cc.Labels[types.AddressQualifier]
+	if container.IsOwnedBySkupper(cc.Labels) && !isServiceContainer {
 		pp.ProcessRole = &flow.Internal
 	} else {
 		pp.ProcessRole = &flow.External
diff --git a/pkg/domain/podman/site.go b/pkg/domain/podman/site.go
index 70411257..499dd3d0 100644
--- a/pkg/domain/podman/site.go
+++ b/pkg/domain/podman/site.go
@@ -16,6 +16,7 @@ import (
 	"github.com/skupperproject/skupper/pkg/qdr"
 	"github.com/skupperproject/skupper/pkg/utils"
 	"github.com/skupperproject/skupper/pkg/version"
+	yaml "gopkg.in/yaml.v3"
 	"k8s.io/apimachinery/pkg/util/rand"
 )
 
@@ -36,6 +37,7 @@ type Site struct {
 	ConsolePassword              string
 	FlowCollectorRecordTtl       time.Duration
 	RouterOpts                   types.RouterOptions
+	PrometheusOpts               types.PrometheusServerOptions
 }
 
 func (s *Site) GetPlatform() string {
@@ -63,6 +65,13 @@ type SiteHandler struct {
 	endpoint string
 }
 
+func NewSitePodmanHandlerFromCli(cli *podman.PodmanRestClient) *SiteHandler {
+	return &SiteHandler{
+		cli:      cli,
+		endpoint: cli.GetEndpoint(),
+	}
+}
+
 func NewSitePodmanHandler(endpoint string) (*SiteHandler, error) {
 	if endpoint == "" {
 		podmanCfg, err := NewPodmanConfigFileHandler().GetConfig()
@@ -120,6 +129,7 @@ func (s *SiteHandler) ConfigurePodmanDeployments(site *Site) {
 	site.Deployments = append(site.Deployments, s.prepareControllerDeployment(site))
 	if site.EnableFlowCollector {
 		site.Deployments = append(site.Deployments, s.prepareFlowCollectorDeployment(site))
+		site.Deployments = append(site.Deployments, s.preparePrometheusDeployment(site))
 	}
 }
 
@@ -293,6 +303,11 @@ func (s *SiteHandler) Create(site domain.Site) error {
 		return err
 	}
 
+	// Create prometheus config
+	if err = s.createPrometheusConfigFiles(podmanSite); err != nil {
+		return err
+	}
+
 	// Deploy container(s)
 	deployHandler := NewSkupperDeploymentHandlerPodman(s.cli)
 	for _, depl := range podmanSite.GetDeployments() {
@@ -333,7 +348,6 @@ func (s *SiteHandler) canCreate(site *Site) error {
 	if s.cli == nil {
 		cli, err := podman.NewPodmanClient(site.PodmanEndpoint, "")
 		if err != nil {
-			// TODO try to start podman's user service instance?
 			return fmt.Errorf("unable to communicate with podman service through %s - %v", site.PodmanEndpoint, err)
 		}
 		s.cli = cli
@@ -514,11 +528,16 @@ func (s *SiteHandler) Get() (domain.Site, error) {
 				site.FlowCollectorRecordTtl, _ = time.ParseDuration(c.Env["FLOW_RECORD_TTL"])
 				user, password, err := s.getConsoleUserPass()
 				if err != nil {
-					fmt.Println("error retrieving console user and password - %w", err)
+					fmt.Println("error retrieving console user and password -", err)
 				}
 				site.ConsoleUser = user
 				site.ConsolePassword = password
 			case *domain.Controller:
+			case *domain.Prometheus:
+				site.PrometheusOpts, err = s.getPrometheusServerOptions()
+				if err != nil {
+					fmt.Println("error retrieving prometheus options -", err)
+				}
 			}
 		}
 	}
@@ -787,3 +806,101 @@ func (s *SiteHandler) prepareControllerDeployment(site *Site) *SkupperDeployment
 
 	return ctrlDeployment
 }
+
+func (s *SiteHandler) preparePrometheusDeployment(site *Site) domain.SkupperDeployment {
+	// Prometheus Server Deployment
+	volumeMounts := map[string]string{
+		"prometheus-server-config":  "/etc/prometheus",
+		"prometheus-storage-volume": "/prometheus",
+	}
+	prometheusComponent := &domain.Prometheus{
+		// TODO ADD Labels
+		Labels: map[string]string{},
+	}
+	prometheusDeployment := &SkupperDeployment{
+		Name: types.PrometheusDeploymentName,
+		SkupperDeploymentCommon: &domain.SkupperDeploymentCommon{
+			Components: []domain.SkupperComponent{
+				prometheusComponent,
+			},
+		},
+		Aliases:        []string{types.PrometheusDeploymentName},
+		VolumeMounts:   volumeMounts,
+		Networks:       []string{site.ContainerNetwork},
+		SELinuxDisable: true,
+	}
+	return prometheusDeployment
+}
+
+func (s *SiteHandler) getPrometheusServerOptions() (types.PrometheusServerOptions, error) {
+	var prometheusConfig types.PrometheusServerOptions
+	v, err := s.cli.VolumeInspect("prometheus-server-config")
+	if err != nil {
+		return prometheusConfig, err
+	}
+	prometheusConfigStr, err := v.ReadFile("skupper-prometheus.yml")
+	if err != nil {
+		return prometheusConfig, fmt.Errorf("error reading skupper-prometheus.yml - %s", err)
+	}
+	err = yaml.Unmarshal([]byte(prometheusConfigStr), &prometheusConfig)
+	if err != nil {
+		return prometheusConfig, fmt.Errorf("error parsing prometheus options - %s", err)
+	}
+	return prometheusConfig, nil
+}
+
+func (s *SiteHandler) savePrometheusServerConfigFile(name string, data interface{}) error {
+	v, err := s.cli.VolumeInspect("prometheus-server-config")
+	if err != nil {
+		return err
+	}
+	var dataStr string
+	var ok bool
+	if dataStr, ok = data.(string); !ok {
+		var yamlData []byte
+		yamlData, err = yaml.Marshal(data)
+		if err != nil {
+			return fmt.Errorf("error serializing prometheus options - %s", err)
+		}
+		dataStr = string(yamlData)
+	}
+	_, err = v.CreateFile(name, []byte(dataStr), true)
+	return err
+}
+
+func (s *SiteHandler) createPrometheusConfigFiles(site *Site) error {
+	promInfo := config.PrometheusInfo{
+		BasicAuth:   false,
+		TlsAuth:     false,
+		ServiceName: types.FlowCollectorContainerName,
+		Port:        strconv.Itoa(int(types.FlowCollectorDefaultServicePort)),
+		User:        utils.DefaultStr(site.PrometheusOpts.User, "admin"),
+		Password:    utils.DefaultStr(site.PrometheusOpts.Password, "admin"),
+		Hash:        "",
+	}
+
+	if site.PrometheusOpts.AuthMode == string(types.PrometheusAuthModeBasic) {
+		promInfo.BasicAuth = true
+		promInfo.User = site.PrometheusOpts.User
+		promInfo.Password = site.PrometheusOpts.Password
+		hash, _ := config.HashPrometheusPassword(promInfo.Password)
+		promInfo.Hash = string(hash)
+	} else if site.PrometheusOpts.AuthMode == string(types.PrometheusAuthModeTls) {
+		promInfo.TlsAuth = true
+	}
+
+	prometheusConfigFiles := map[string]interface{}{
+		"skupper-prometheus.yml": site.PrometheusOpts,
+		"prometheus.yml":         config.ScrapeConfigForPrometheus(promInfo),
+		"web-config.yml":         config.ScrapeWebConfigForPrometheus(promInfo),
+	}
+
+	var err error
+	for name, data := range prometheusConfigFiles {
+		err = s.savePrometheusServerConfigFile(name, data)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/pkg/domain/podman/update/common.go b/pkg/domain/podman/update/common.go
new file mode 100644
index 00000000..ced175d6
--- /dev/null
+++ b/pkg/domain/podman/update/common.go
@@ -0,0 +1,165 @@
+package update
+
+import (
+	"fmt"
+
+	"github.com/skupperproject/skupper/api/types"
+	clientpodman "github.com/skupperproject/skupper/client/podman"
+	"github.com/skupperproject/skupper/pkg/domain"
+	"github.com/skupperproject/skupper/pkg/domain/podman"
+	"github.com/skupperproject/skupper/pkg/images"
+	"github.com/skupperproject/skupper/pkg/utils"
+	"github.com/skupperproject/skupper/pkg/version"
+)
+
+func NewVersionUpdateTask(cli *clientpodman.PodmanRestClient) *VersionUpdateTask {
+	return &VersionUpdateTask{
+		cli:     cli,
+		version: version.Version,
+	}
+}
+
+type VersionUpdateTask struct {
+	cli     *clientpodman.PodmanRestClient
+	version string
+}
+
+func (v *VersionUpdateTask) Info() string {
+	return "Updates current version number"
+}
+
+func (v *VersionUpdateTask) AppliesTo(siteVersion string) bool {
+	curVersion := utils.ParseVersion(siteVersion)
+	return !(&curVersion).IsUndefined() && utils.LessRecentThanVersion(siteVersion, v.version)
+}
+
+func (v *VersionUpdateTask) Version() string {
+	return "*"
+}
+
+func (v *VersionUpdateTask) Priority() domain.UpdatePriority {
+	return domain.PriorityLast
+}
+
+func (v *VersionUpdateTask) Run() *domain.UpdateResult {
+	var res = &domain.UpdateResult{}
+	ch := podman.NewRouterConfigHandlerPodman(v.cli)
+	cfg, err := ch.GetRouterConfig()
+	if err != nil {
+		res.AddErrors(fmt.Errorf("error retrieving router config: %s", err))
+		return res
+	}
+	siteMeta := cfg.GetSiteMetadata()
+	siteMeta.Version = v.version
+	cfg.SetSiteMetadata(&siteMeta)
+	if err = ch.SaveRouterConfig(cfg); err != nil {
+		res.AddErrors(fmt.Errorf("error saving router config: %s", err))
+		return res
+	}
+	res.AddChange(fmt.Sprintf("updated site version to %s", v.version))
+	return res
+}
+
+func NewContainerImagesTask(cli *clientpodman.PodmanRestClient) *ContainerImagesTask {
+	return &ContainerImagesTask{
+		cli:     cli,
+		version: version.Version,
+	}
+}
+
+type ContainerImagesTask struct {
+	cli     *clientpodman.PodmanRestClient
+	version string
+}
+
+func (u *ContainerImagesTask) Info() string {
+	return "Updates skupper podman container images"
+}
+
+func (u *ContainerImagesTask) AppliesTo(siteVersion string) bool {
+	curVersion := utils.ParseVersion(siteVersion)
+	return !(&curVersion).IsUndefined() && utils.LessRecentThanVersion(siteVersion, u.version)
+}
+
+func (u *ContainerImagesTask) Version() string {
+	return "*"
+}
+
+func (u *ContainerImagesTask) Priority() domain.UpdatePriority {
+	return domain.PriorityNormal
+}
+
+func (u *ContainerImagesTask) Run() *domain.UpdateResult {
+	var result = &domain.UpdateResult{}
+	sh := podman.NewSitePodmanHandlerFromCli(u.cli)
+	site, err := sh.Get()
+	if err != nil {
+		result.AddErrors(fmt.Errorf("error retrieving site info: %s", err))
+		return result
+	}
+	// updating images for all running components
+	for _, dep := range site.GetDeployments() {
+		for _, cmp := range dep.GetComponents() {
+			var image string
+			switch cmp.Name() {
+			case types.TransportDeploymentName:
+				image = images.GetRouterImageName()
+			case types.FlowCollectorContainerName:
+				image = images.GetFlowCollectorImageName()
+			case types.ControllerPodmanContainerName:
+				image = images.GetControllerPodmanImageName()
+			case types.PrometheusDeploymentName:
+				image = images.GetPrometheusServerImageName()
+			}
+			if image != cmp.GetImage() {
+				_, err = u.cli.ContainerUpdateImage(cmp.Name(), image)
+				if err != nil {
+					result.AddErrors(fmt.Errorf("error updating container: %s - image: %s - %s",
+						cmp.Name(), image, err))
+					return result
+				}
+				result.AddChange(fmt.Sprintf("container updated: %s - image: %s", cmp.Name(), image))
+			}
+		}
+	}
+	// updating service containers
+	updSvcResult := u.updateServiceContainers()
+	if len(updSvcResult.Errors) > 0 {
+		result.AddErrors(updSvcResult.Errors...)
+		return result
+	}
+	if updSvcResult.Changed() {
+		result.AddChange(updSvcResult.GetChanges()...)
+	}
+	return result
+}
+
+func (u *ContainerImagesTask) updateServiceContainers() domain.UpdateResult {
+	var result domain.UpdateResult
+	sh := podman.NewServiceHandlerPodman(u.cli)
+	services, err := sh.List()
+	if err != nil {
+		result.AddErrors(fmt.Errorf("error listing services: %s", err))
+		return result
+	}
+	for _, svc := range services {
+		svcPodman := svc.(*podman.Service)
+		c, err := u.cli.ContainerInspect(svcPodman.ContainerName)
+		if err != nil {
+			result.AddErrors(fmt.Errorf("error retrieving container info for %s: %s",
+				svcPodman.ContainerName, err))
+			return result
+		}
+		if c.Image != images.GetRouterImageName() {
+			_, err = u.cli.ContainerUpdateImage(svcPodman.ContainerName, images.GetRouterImageName())
+			if err != nil {
+				result.AddErrors(fmt.Errorf("error updating service container image for %s: %s",
+					svcPodman.ContainerName, err))
+				return result
+			}
+			result.AddChange(fmt.Sprintf("service container updated: %s - image: %s",
+				svcPodman.ContainerName, images.GetRouterImageName()))
+		}
+	}
+	return result
+}
diff --git a/pkg/domain/podman/update/skrouterd.json b/pkg/domain/podman/update/skrouterd.json
new file mode 100644
index 00000000..89cc965d
--- /dev/null
+++ b/pkg/domain/podman/update/skrouterd.json
@@ -0,0 +1,99 @@
+[
+  [
+    "router",
+    {
+      "id": "fedora-myuser",
+      "mode": "interior",
+      "helloMaxAgeSeconds": "3",
+      "metadata": "{\"id\":\"fd70682b-c6fc-472d-8b6d-f187a167cd4e\",\"version\":\"1.4.2\",\"platform\":\"podman\"}"
+    }
+  ],
+  [
+    "sslProfile",
+    {
+      "name": "skupper-amqps",
+      "certFile": "/etc/skupper-router-certs/skupper-amqps/tls.crt",
+      "privateKeyFile": "/etc/skupper-router-certs/skupper-amqps/tls.key",
+      "caCertFile": "/etc/skupper-router-certs/skupper-amqps/ca.crt"
+    }
+  ],
+  [
+    "sslProfile",
+    {
+      "name": "skupper-service-client",
+      "caCertFile": "/etc/skupper-router-certs/skupper-service-client/ca.crt"
+    }
+  ],
+  [
+    "sslProfile",
+    {
+      "name": "skupper-internal",
+      "certFile": "/etc/skupper-router-certs/skupper-internal/tls.crt",
+      "privateKeyFile": "/etc/skupper-router-certs/skupper-internal/tls.key",
+      "caCertFile": "/etc/skupper-router-certs/skupper-internal/ca.crt"
+    }
+  ],
+  [
+    "listener",
+    {
+      "name": "edge-listener",
+      "role": "edge",
+      "port": 45671,
+      "sslProfile": "skupper-internal",
+      "saslMechanisms": "EXTERNAL",
+      "authenticatePeer": true,
+      "maxFrameSize": 16384,
+      "maxSessionFrames": 640
+    }
+  ],
+  [
+    "listener",
+    {
+      "name": "@9090",
+      "role": "normal",
+      "port": 9090,
+      "http": true,
+      "httpRootDir": "disabled",
+      "healthz": true,
+      "metrics": true
+    }
+  ],
+  [
+    "listener",
+    {
+      "name": "amqp",
+      "host": "localhost",
+      "port": 5672
+    }
+  ],
+  [
+    "listener",
+    {
+      "name": "amqps",
+      "port": 5671,
+      "sslProfile": "skupper-amqps",
+      "saslMechanisms": "EXTERNAL",
+      "authenticatePeer": true
+    }
+  ],
+  [
+    "listener",
+    {
+      "name": "interior-listener",
+      "role": "inter-router",
+      "port": 55671,
+      "sslProfile": "skupper-internal",
+      "saslMechanisms": "EXTERNAL",
+      "authenticatePeer": true,
+      "maxFrameSize": 16384,
+      "maxSessionFrames": 640
+    }
+  ],
+  [
+    "address",
+    {
+      "prefix": "mc",
+      "distribution": "multicast"
+    }
+  ]
+]
diff --git a/pkg/domain/podman/update/skupper-services.json b/pkg/domain/podman/update/skupper-services.json
new file mode 100644
index 00000000..db56b574
--- /dev/null
+++ b/pkg/domain/podman/update/skupper-services.json
@@ -0,0 +1,1 @@
+{"nginx":{"address":"nginx","protocol":"tcp","ports":[8080],"exposeIngress":"","targets":[{"name":"*domain.EgressResolverHost={\"host\":\"nginx\",\"ports\":{\"8080\":8080}}","targetPorts":{"8080":8080},"service":"nginx"}]}}
\ No newline at end of file
diff --git a/pkg/domain/skupper_component.go b/pkg/domain/skupper_component.go
index 5075756d..82c70962 100644
--- a/pkg/domain/skupper_component.go
+++ b/pkg/domain/skupper_component.go
@@ -3,6 +3,7 @@ package domain
 import (
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/pkg/images"
+	"github.com/skupperproject/skupper/test/utils"
 )
 
 type SkupperComponent interface {
@@ -31,7 +32,7 @@ func (r *Router) Name() string {
 }
 
 func (r *Router) GetImage() string {
-	return images.GetRouterImageName()
+	return utils.StrDefault(images.GetRouterImageName(), r.Image)
 }
 
 func (r *Router) SetImage(image string) {
@@ -68,7 +69,7 @@ func (r *FlowCollector) Name() string {
 }
 
 func (r *FlowCollector) GetImage() string {
-	return images.GetFlowCollectorImageName()
+	return utils.StrDefault(images.GetFlowCollectorImageName(), r.Image)
 }
 
 func (r *FlowCollector) SetImage(image string) {
@@ -105,7 +106,7 @@ func (s *Controller) Name() string {
 }
 
 func (s *Controller) GetImage() string {
-	return images.GetControllerPodmanImageName()
+	return utils.StrDefault(images.GetControllerPodmanImageName(), s.Image)
 }
 
 func (s *Controller) SetImage(image string) {
@@ -129,3 +130,40 @@ func (s *Controller) GetLabels() map[string]string {
 func (s *Controller) GetSiteIngresses() []SiteIngress {
 	return s.SiteIngresses
 }
+
+type Prometheus struct {
+	Image         string
+	Env           map[string]string
+	Labels        map[string]string
+	SiteIngresses []SiteIngress
+}
+
+func (s *Prometheus) Name() string {
+	return types.PrometheusDeploymentName
+}
+
+func (s *Prometheus) GetImage() string {
+	return images.GetPrometheusServerImageName()
+}
+
+func (s *Prometheus) SetImage(image string) {
+	s.Image = image
+}
+
+func (s *Prometheus) GetEnv() map[string]string {
+	if s.Env == nil {
+		s.Env = map[string]string{}
+	}
+	return s.Env
+}
+
+func (s *Prometheus) GetLabels() map[string]string {
+	if s.Labels == nil {
+		s.Labels = map[string]string{}
+	}
+	return s.Labels
+}
+
+func (s *Prometheus) GetSiteIngresses() []SiteIngress {
+	return s.SiteIngresses
+}
diff --git a/pkg/domain/update.go b/pkg/domain/update.go
new file mode 100644
index 00000000..d65974c6
--- /dev/null
+++ b/pkg/domain/update.go
@@ -0,0 +1,208 @@
+package domain
+
+import (
+	"fmt"
+	"reflect"
+	"sort"
+
+	"github.com/skupperproject/skupper/pkg/utils"
+)
+
+type UpdatePriority int
+
+const (
+	PriorityFirst UpdatePriority = iota
+	PriorityNormal
+	PriorityLast
+)
+
+// UpdateResult contains errors, warnings and changes applied by tasks.
+// It also contains a set of PostTasks that can be populated by tasks.
+// The PostTasks will only be evaluated after all main tasks complete.
+type UpdateResult struct {
+	Errors    []error
+	warnings  []string
+	changes   []string
+	PostTasks []UpdateTask
+}
+
+func (u *UpdateResult) AddErrors(err ...error) {
+	u.Errors = append(u.Errors, err...)
+}
+
+func (u *UpdateResult) AddWarnings(msg ...string) {
+	u.warnings = append(u.warnings, msg...)
+}
+
+func (u *UpdateResult) GetWarnings() []string {
+	return u.warnings
+}
+
+func (u *UpdateResult) Changed() bool {
+	return len(u.changes) > 0
+}
+
+func (u *UpdateResult) AddChange(change ...string) {
+	u.changes = append(u.changes, change...)
+}
+
+func (u *UpdateResult) GetChanges() []string {
+	return u.changes
+}
+
+type UpdateTask interface {
+	// Info describes the update task
+	Info() string
+	// AppliesTo determines whether it should be executed or not
+	AppliesTo(siteVersion string) bool
+	// Version returns the version it has been introduced (use * when not constrained to a given version)
+	Version() string
+	// Priority determines how tasks will be sorted by the UpdateProcessor
+	Priority() UpdatePriority
+	// Run method is where the update task is done
+	Run() *UpdateResult
+}
+
+type UpdateProcessor struct {
+	tasks     []UpdateTask
+	postTasks []UpdateTask
+	changes   []string
+	Verbose   bool
+	DryRun    bool
+}
+
+func (p *UpdateProcessor) Println(a ...any) {
+	if p.Verbose {
+		fmt.Println(a...)
+	}
+}
+
+func (p *UpdateProcessor) Printf(format string, a ...any) {
+	if p.Verbose {
+		fmt.Printf(format, a...)
+	}
+}
+
+// RegisterTasks registers tasks to be evaluated and processed
+func (p *UpdateProcessor) RegisterTasks(task ...UpdateTask) {
+	p.tasks = append(p.tasks, task...)
+}
+
+// registerPostTasks register post tasks to be evaluated and processed
+// after all tasks have completed.
+// The Post Tasks are processed only once.
+func (p *UpdateProcessor) registerPostTasks(tasks ...UpdateTask) {
+	for _, task := range tasks {
+		add := true
+		for _, postTask := range p.postTasks {
+			if task == postTask {
+				add = false
+				break
+			}
+		}
+		if add {
+			p.postTasks = append(p.postTasks, task)
+		}
+	}
+}
+
+func (p *UpdateProcessor) Process(siteVersion string) error {
+	err := p.process(siteVersion, p.tasks)
+	if err != nil {
+		return err
+	}
+	if len(p.postTasks) > 0 {
+		p.Println("Post update tasks")
+		return p.process(siteVersion, p.postTasks)
+	}
+	if len(p.changes) > 0 {
+		fmt.Println("Skupper is now updated for '" + utils.ReadUsername() + "'.")
+	} else {
+		fmt.Println("No update required for '" + utils.ReadUsername() + "'.")
+	}
+	return nil
+}
+
+func (p *UpdateProcessor) process(siteVersion string, tasks []UpdateTask) error {
+	var validTasks []UpdateTask
+	title := "Task"
+	postTasks := false
+	if reflect.DeepEqual(tasks, p.postTasks) {
+		title = "Post task"
+		postTasks = true
+	}
+	for _, task := range tasks {
+		if task.AppliesTo(siteVersion) {
+			validTasks = append(validTasks, task)
+		}
+	}
+	totalTasks := len(validTasks)
+
+	if totalTasks > 0 {
+		// sorting update tasks by version and priority
+		sort.SliceStable(validTasks, func(i, j int) bool {
+			t1 := validTasks[i]
+			t2 := validTasks[j]
+			sameVersion := utils.EquivalentVersion(t1.Version(), t2.Version())
+			if !sameVersion && (t1.Version() != "*" && t2.Version() != "*") {
+				return utils.LessRecentThanVersion(t1.Version(), t2.Version())
+			} else if t1.Version() == "*" {
+				return false
+			} else if t2.Version() == "*" {
+				return true
+			}
+			return t1.Priority() < t2.Priority()
+		})
+	} else {
+		p.Println("Nothing to do")
+		return nil
+	}
+
+	// running update tasks
+	var errors []error
+	var warnings []string
+	for i, task := range validTasks {
+		version := task.Version()
+		if version == "*" {
+			version = "all"
+		}
+		p.Printf("%s: %d/%d - version: %s - priority: %d - %s\n",
+			title, i+1, totalTasks, version, task.Priority(), task.Info())
+		var result *UpdateResult
+		if !p.DryRun {
+			result = task.Run()
+			if result == nil {
+				result = &UpdateResult{}
+			}
+		} else {
+			result = &UpdateResult{}
+		}
+		if len(result.warnings) > 0 {
+			warnings = append(warnings, result.warnings...)
+			p.Printf("  -> WARNING: %v\n", result.warnings)
+		}
+		if len(result.Errors) > 0 {
+			errors = append(errors, result.Errors...)
+			p.Printf("  -> ERROR: %v\n", result.Errors)
+			p.Println("     unable to proceed")
+			return fmt.Errorf("unable to proceed due to the following errors (%d): %v", len(errors), errors)
+		}
+		// post tasks are only processed once, for main tasks
+		if !postTasks && len(result.PostTasks) > 0 {
+			p.registerPostTasks(result.PostTasks...)
+		}
+		if len(result.GetChanges()) > 0 {
+			p.changes = append(p.changes, result.GetChanges()...)
+		}
+		p.Println()
+	}
+
+	// warnings are considered as informative only and will not
+	// cause the update command to fail (rc != 0)
+	if !postTasks && len(warnings) > 0 {
+		errMsg := fmt.Sprintf("Update completed with (%d) warnings: %v", len(warnings), warnings)
+		p.Println(errMsg)
+	}
+
+	return nil
+}
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/contour.go b/pkg/kube/contour.go
index d0c5701f..4f266c06 100644
--- a/pkg/kube/contour.go
+++ b/pkg/kube/contour.go
@@ -86,11 +86,12 @@ func GetContourProxy(client dynamic.Interface, namespace string, name string) (*
 	return &proxy, nil
 }
 
-func CreateContourProxy(client dynamic.Interface, namespace string, name string, def IngressRoute, owner *metav1.OwnerReference) error {
+func CreateContourProxy(client dynamic.Interface, namespace string, name string, def IngressRoute, labels map[string]string, owner *metav1.OwnerReference) error {
 	obj := unstructured.Unstructured{}
 	obj.SetGroupVersionKind(httpProxyGVK)
 	obj.SetName(name)
 	obj.SetOwnerReferences([]metav1.OwnerReference{*owner})
+	obj.SetLabels(labels)
 	err := def.writeToContourProxy(&obj)
 	if err != nil {
 		return err
@@ -102,9 +103,9 @@ func CreateContourProxy(client dynamic.Interface, namespace string, name string,
 	return nil
 }
 
-func CreateContourProxies(routes []IngressRoute, owner *metav1.OwnerReference, client dynamic.Interface, namespace string) error {
+func CreateContourProxies(routes []IngressRoute, labels map[string]string, owner *metav1.OwnerReference, client dynamic.Interface, namespace string) error {
 	for _, route := range routes {
-		err := CreateContourProxy(client, namespace, route.Name, route, owner)
+		err := CreateContourProxy(client, namespace, route.Name, route, labels, owner)
 		if err != nil && !errors.IsAlreadyExists(err) {
 			return err
 		}
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/kube/ingress.go b/pkg/kube/ingress.go
index 36fbaa7c..99278550 100644
--- a/pkg/kube/ingress.go
+++ b/pkg/kube/ingress.go
@@ -111,7 +111,7 @@ func UpdateIngressRuleServiceName(ingressName string, hostPrefix string, service
 	}
 }
 
-func CreateIngress(name string, routes []IngressRoute, isNginx bool, sslPassthrough bool, ownerRefs []metav1.OwnerReference, namespace string, annotations map[string]string, clients Clients) error {
+func CreateIngress(name string, routes []IngressRoute, isNginx bool, sslPassthrough bool, ownerRefs []metav1.OwnerReference, namespace string, annotations map[string]string, labels map[string]string, clients Clients) error {
 	if useV1API(clients.GetDiscoveryClient()) {
 		if isNginx {
 			if annotations == nil {
@@ -125,13 +125,14 @@ func CreateIngress(name string, routes []IngressRoute, isNginx bool, sslPassthro
 				resolve = true
 			}
 		}
-		return ensureIngressRoutesV1(clients.GetDynamicClient(), namespace, name, routes, annotations, ownerRefs, resolve)
+		return ensureIngressRoutesV1(clients.GetDynamicClient(), namespace, name, routes, annotations, labels, ownerRefs, resolve)
 	}
 	cli := clients.GetKubeClient()
 	ingress := networkingv1.Ingress{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:        name,
 			Annotations: map[string]string{},
+			Labels:      labels,
 		},
 		Spec: networkingv1.IngressSpec{
 			Rules: []networkingv1.IngressRule{},
@@ -243,7 +244,7 @@ func appendIngressHostSuffix(desired []IngressRoute, suffix string) []IngressRou
 	return updated
 }
 
-func ensureIngressRoutesV1(client dynamic.Interface, namespace string, name string, routes []IngressRoute, annotations map[string]string, ownerRefs []metav1.OwnerReference, resolve bool) error {
+func ensureIngressRoutesV1(client dynamic.Interface, namespace string, name string, routes []IngressRoute, annotations map[string]string, labels map[string]string, ownerRefs []metav1.OwnerReference, resolve bool) error {
 	obj, err := client.Resource(ingressResource).Namespace(namespace).Get(context.TODO(), name, metav1.GetOptions{})
 	if errors.IsNotFound(err) {
 		obj = &unstructured.Unstructured{}
@@ -255,6 +256,7 @@ func ensureIngressRoutesV1(client dynamic.Interface, namespace string, name stri
 		obj.SetName(name)
 		obj.SetOwnerReferences(ownerRefs)
 		obj.SetAnnotations(annotations)
+		obj.SetLabels(labels)
 		err := writeIngressRules(routes, obj)
 		if err != nil {
 			return err
diff --git a/pkg/kube/qdr/tls_handler.go b/pkg/kube/qdr/tls_handler.go
index 4a047d24..5ddff82c 100644
--- a/pkg/kube/qdr/tls_handler.go
+++ b/pkg/kube/qdr/tls_handler.go
@@ -9,6 +9,7 @@ import (
 	"github.com/skupperproject/skupper/pkg/kube"
 	"github.com/skupperproject/skupper/pkg/qdr"
 	"github.com/skupperproject/skupper/pkg/service"
+	"github.com/skupperproject/skupper/pkg/site"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes"
@@ -144,6 +145,12 @@ func generateNewSecret(support TlsServiceSupport, tlsManager *TlsManager) (*core
 		return nil, err
 	}
 
+	siteConfig, err := tlsManager.getConfigMap(types.SiteConfigMapName)
+	if err != nil {
+		return nil, err
+	}
+	_, labels := site.GetSiteAnnotationsAndLabels(siteConfig)
+
 	serviceCredential := types.Credential{
 		CA:          types.ServiceCaSecret,
 		Name:        support.Credentials,
@@ -151,6 +158,7 @@ func generateNewSecret(support TlsServiceSupport, tlsManager *TlsManager) (*core
 		Hosts:       []string{support.Address},
 		ConnectJson: false,
 		Post:        false,
+		Labels:      labels,
 	}
 
 	ownerReference := metav1.OwnerReference{
@@ -182,8 +190,11 @@ func (manager *TlsManager) GetSecret(name string) (*corev1.Secret, error) {
 }
 
 func (manager *TlsManager) GetConfigMap() (*corev1.ConfigMap, error) {
+	return manager.getConfigMap(types.TransportConfigMapName)
+}
 
-	result, err := manager.KubeClient.CoreV1().ConfigMaps(manager.Namespace).Get(context.TODO(), types.TransportConfigMapName, metav1.GetOptions{})
+func (manager *TlsManager) getConfigMap(name string) (*corev1.ConfigMap, error) {
+	result, err := manager.KubeClient.CoreV1().ConfigMaps(manager.Namespace).Get(context.TODO(), name, metav1.GetOptions{})
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/kube/secrets.go b/pkg/kube/secrets.go
index 70af890f..c7102fed 100644
--- a/pkg/kube/secrets.go
+++ b/pkg/kube/secrets.go
@@ -22,6 +22,7 @@ func NewCertAuthority(ca types.CertAuthority, owner *metav1.OwnerReference, name
 		return existing, nil
 	} else if errors.IsNotFound(err) {
 		newCA := certs.GenerateCASecret(ca.Name, ca.Name)
+		newCA.Labels = ca.Labels
 		if owner != nil {
 			newCA.ObjectMeta.OwnerReferences = []metav1.OwnerReference{
 				*owner,
@@ -80,6 +81,7 @@ func NewSecret(cred types.Credential, owner *metav1.OwnerReference, namespace st
 	}
 
 	secret = PrepareNewSecret(cred, caSecret, connectJsonHost)
+	secret.ObjectMeta.Labels = cred.Labels
 
 	if owner != nil {
 		secret.ObjectMeta.OwnerReferences = []metav1.OwnerReference{
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 {
diff --git a/pkg/site/site.go b/pkg/site/site.go
index 8e99bcb3..904ddb54 100644
--- a/pkg/site/site.go
+++ b/pkg/site/site.go
@@ -780,6 +780,16 @@ func ReadSiteConfig(siteConfig *corev1.ConfigMap, namespace string, defaultIngre
 		result.Spec.PrometheusServer.PodAnnotations = asMap(splitWithEscaping(prometheusPodAnnotations, ',', '\\'))
 	}
 
+	annotations, labels := GetSiteAnnotationsAndLabels(siteConfig)
+	result.Spec.Annotations = annotations
+	result.Spec.Labels = labels
+	if len(errs) > 0 {
+		return &result, fmt.Errorf(strings.Join(errs, ", "))
+	}
+	return &result, nil
+}
+
+func GetSiteAnnotationsAndLabels(siteConfig *corev1.ConfigMap) (map[string]string, map[string]string) {
 	annotationExclusions := []string{}
 	labelExclusions := []string{}
 	annotations := map[string]string{}
@@ -795,7 +805,6 @@ func ReadSiteConfig(siteConfig *corev1.ConfigMap, namespace string, defaultIngre
 	for _, key := range annotationExclusions {
 		delete(annotations, key)
 	}
-	result.Spec.Annotations = annotations
 	labels := map[string]string{}
 	for key, value := range siteConfig.ObjectMeta.Labels {
 		if key != types.SiteControllerIgnore {
@@ -805,11 +814,7 @@ func ReadSiteConfig(siteConfig *corev1.ConfigMap, namespace string, defaultIngre
 	for _, key := range labelExclusions {
 		delete(labels, key)
 	}
-	result.Spec.Labels = labels
-	if len(errs) > 0 {
-		return &result, fmt.Errorf(strings.Join(errs, ", "))
-	}
-	return &result, nil
+	return annotations, labels
 }
 
 func UpdateLogging(config types.SiteConfigSpec, configmap *corev1.ConfigMap) bool {
diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go
index 8b5b8bfb..bc7658f8 100644
--- a/pkg/utils/utils.go
+++ b/pkg/utils/utils.go
@@ -18,6 +18,7 @@ import (
 	"crypto/rand"
 	"io"
 	"os"
+	"os/user"
 	"regexp"
 	"strings"
 )
@@ -162,3 +163,11 @@ func DefaultNumber[T Number](values ...T) T {
 	}
 	return DefaultNumber(values[1:]...)
 }
+
+func ReadUsername() string {
+	u, err := user.Current()
+	if err != nil {
+		return DefaultStr(os.Getenv("USER"), os.Getenv("USERNAME"))
+	}
+	return strings.Join(strings.Fields(u.Username), "")
+}


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/client/podman/container_test.go b/client/podman/container_test.go
index 65540dc1..14c077ae 100644
--- a/client/podman/container_test.go
+++ b/client/podman/container_test.go
@@ -2,6 +2,7 @@ package podman
 
 import (
 	"context"
+	"fmt"
 	"os"
 	"path"
 	"strings"
@@ -9,8 +10,10 @@ import (
 	"testing"
 	"time"
 
+	"github.com/go-openapi/runtime"
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/client/container"
+	"github.com/skupperproject/skupper/client/generated/libpod/client/containers"
 	"github.com/skupperproject/skupper/pkg/images"
 	"github.com/skupperproject/skupper/pkg/utils"
 	"gotest.tools/assert"
@@ -139,7 +142,7 @@ func TestContainer(t *testing.T) {
 	})
 
 	// Listing containers
-	t.Run("container-inspect", func(t *testing.T) {
+	containerInspectTest := func(t *testing.T) {
 		c, err := cli.ContainerInspect(name)
 		assert.Assert(t, err, "error inspecting container")
 
@@ -161,7 +164,8 @@ func TestContainer(t *testing.T) {
 		assert.Equal(t, sourcePort, p.Host)
 		assert.Equal(t, hostIP, p.HostIP)
 		assert.Equal(t, targetPort, p.Target)
-	})
+	}
+	t.Run("container-inspect", containerInspectTest)
 
 	t.Run("container-exec", func(t *testing.T) {
 		out, err := cli.ContainerExec(name, strings.Split("ls -1 /app", " "))
@@ -186,6 +190,16 @@ func TestContainer(t *testing.T) {
 		assert.Equal(t, name, c.NetworkNames()[0])
 	})
 
+	// Updating container image
+	image = strings.ReplaceAll(image, ":main", ":latest")
+	t.Run("container-update-image", func(t *testing.T) {
+		c, err := cli.ContainerUpdateImage(name, image)
+		assert.Assert(t, err)
+		assert.Equal(t, c.Image, image)
+		containerInspectTest(t)
+	})
+	t.Run("container-inspect-after-image-update", containerInspectTest)
+
 	// Cleaning up
 	t.Run("cleanup", func(t *testing.T) {
 		var cleanupErrors []error
@@ -230,3 +244,144 @@ func ValidateStrings(t *testing.T, original []string, final []string) {
 		assert.Assert(t, utils.StringSliceContains(final, v), "string not found %s", v)
 	}
 }
+
+func TestContainerUpdateMock(t *testing.T) {
+	image := images.GetServiceControllerImageName()
+	cli := NewPodmanClientMock(mockContainers(image))
+
+	// starting the container
+	assert.Assert(t, cli.ContainerStart("my-container"))
+
+	// retrieving container
+	cc, err := cli.ContainerInspect("my-container")
+	assert.Assert(t, err)
+	startedAt := cc.StartedAt
+
+	newImage := strings.Replace(image, ":main", ":updated", -1)
+	_, err = cli.ContainerUpdateImage("my-container", newImage)
+	assert.Assert(t, err)
+
+	// validating that the container has the new image and has been restarted
+	cc, err = cli.ContainerInspect("my-container")
+	assert.Assert(t, err)
+	assert.Assert(t, time.Now().After(startedAt))
+	assert.Equal(t, cc.Image, newImage)
+
+	// assert that there is no other container left
+	cl, err := cli.ContainerList()
+	assert.Assert(t, err)
+	assert.Equal(t, len(cl), 1)
+}
+
+func TestContainerUpdateErrorMock(t *testing.T) {
+	image := images.GetServiceControllerImageName()
+	newImage := strings.Replace(image, ":main", ":updated", -1)
+	cli := NewPodmanClientMock(mockContainers(image))
+	mock := cli.RestClient.(*RestClientMock)
+
+	tests := []struct {
+		name      string
+		errorHook func(operation *runtime.ClientOperation) error
+	}{{
+		// must stop and remove the new container
+		name: "error-stopping-old-container",
+		errorHook: func(operation *runtime.ClientOperation) error {
+			if operation.ID == "ContainerStopLibpod" {
+				params := operation.Params.(*containers.ContainerStopLibpodParams)
+				// returns error when stopping the current container
+				if params.Name == "my-container" {
+					return fmt.Errorf("error stopping %q", params.Name)
+				}
+			}
+			return nil
+		},
+	}, {
+		// must start the old container, stop and remove the new one
+		name: "error-renaming-old-container",
+		errorHook: func(operation *runtime.ClientOperation) error {
+			if operation.ID == "ContainerRenameLibpod" {
+				params := operation.Params.(*containers.ContainerRenameLibpodParams)
+				curName := params.PathName
+				newName := params.QueryName
+				// returns error when renaming current container to a backup name
+				if curName == "my-container" {
+					return fmt.Errorf("error renaming container %q to %q", curName, newName)
+				}
+			}
+			return nil
+		},
+	}, {
+		// must rename old container back, start the old container, stop and remove the new one
+		name: "error-renaming-new-container-as-old-container",
+		errorHook: func(operation *runtime.ClientOperation) error {
+			if operation.ID == "ContainerRenameLibpod" {
+				params := operation.Params.(*containers.ContainerRenameLibpodParams)
+				curName := params.PathName
+				newName := params.QueryName
+				// return error when renaming new container to original name
+				if strings.Contains(curName, "-new-") && newName == "my-container" {
+					return fmt.Errorf("error renaming container %q to %q", curName, newName)
+				}
+			}
+			return nil
+		},
+	}, {
+		// must rename old container back, start the old container, stop and remove the new one
+		name: "error-starting-new-container",
+		errorHook: func(operation *runtime.ClientOperation) error {
+			if operation.ID == "ContainerStartLibpod" {
+				params := operation.Params.(*containers.ContainerStartLibpodParams)
+				hasBackupContainer := true
+				for _, c := range mock.Containers {
+					if strings.Contains(c.Name, "-new-") {
+						hasBackupContainer = false
+						break
+					}
+				}
+				// only returns error when starting the updated container
+				if len(mock.Containers) == 2 && hasBackupContainer && params.Name == "my-container" {
+					return fmt.Errorf("error starting %q", params.Name)
+				}
+			}
+			return nil
+		},
+	}}
+
+	// iterate through test scenarios
+	for _, test := range tests {
+		mock.Containers = mockContainers(image)
+		mock.ErrorHook = test.errorHook
+		t.Run(test.name, func(t *testing.T) {
+			_, err := cli.ContainerUpdate("my-container", func(newContainer *container.Container) {
+				newContainer.Image = newImage
+			})
+			assert.Assert(t, err != nil)
+			assert.Equal(t, 1, len(mock.Containers))
+			assert.Equal(t, "my-container", mock.Containers[0].Name)
+			assert.Equal(t, true, mock.Containers[0].Running)
+		})
+	}
+}
+
+func mockContainers(image string) []*container.Container {
+	return []*container.Container{{
+		ID:    "abcd",
+		Name:  "my-container",
+		Image: image,
+		Env:   map[string]string{"var1": "val1", "var2": "val2"},
+		Networks: map[string]container.ContainerNetworkInfo{
+			"skupper": container.ContainerNetworkInfo{
+				ID:        "skupper",
+				IPAddress: "172.17.0.10",
+				Gateway:   "172.17.0.1",
+				Aliases:   []string{"skupper", "service-controller"},
+			},
+		},
+		Ports: []container.Port{
+			{Host: "8888", HostIP: "10.0.0.1", Target: "8080", Protocol: "tcp"},
+		},
+		Running:   true,
+		CreatedAt: time.Now(),
+		StartedAt: time.Now(),
+	}}
+}
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/domain/podman/site_test.go b/pkg/domain/podman/site_test.go
index 8dc72696..61e61c8a 100644
--- a/pkg/domain/podman/site_test.go
+++ b/pkg/domain/podman/site_test.go
@@ -6,6 +6,7 @@ package podman
 import (
 	"testing"
 
+	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/client/container"
 	"github.com/skupperproject/skupper/pkg/domain"
 	"github.com/skupperproject/skupper/pkg/utils"
@@ -41,6 +42,12 @@ func TestSiteHandler(t *testing.T) {
 			AuthMode:            "internal",
 			ConsoleUser:         "internal",
 			ConsolePassword:     "internal",
+			PrometheusOpts: types.PrometheusServerOptions{
+				ExternalServer: "http://10.0.0.1:8080/v1",
+				AuthMode:       "internal",
+				User:           "admin",
+				Password:       "admin",
+			},
 		},
 	}, {
 		name: "flow-collector-internal-auth-ingress-none",
@@ -87,7 +94,7 @@ func TestSiteHandler(t *testing.T) {
 			expIngHosts := 1 + len(scenarioSite.IngressHosts)
 			expDeployments := 2
 			if scenarioSite.EnableFlowCollector {
-				expDeployments += 1
+				expDeployments += 2
 			}
 			assert.Assert(t, len(podmanSite.IngressHosts) == expIngHosts)
 			assert.Equal(t, len(podmanSite.Deployments), expDeployments)
@@ -101,6 +108,10 @@ func TestSiteHandler(t *testing.T) {
 			}
 			assert.Assert(t, len(podmanSite.Credentials) > 0)
 			assert.Assert(t, len(podmanSite.CertAuthorities) > 0)
+			assert.Equal(t, scenarioSite.PrometheusOpts.ExternalServer, podmanSite.PrometheusOpts.ExternalServer)
+			assert.Equal(t, scenarioSite.PrometheusOpts.AuthMode, podmanSite.PrometheusOpts.AuthMode)
+			assert.Equal(t, scenarioSite.PrometheusOpts.User, podmanSite.PrometheusOpts.User)
+			assert.Equal(t, scenarioSite.PrometheusOpts.Password, podmanSite.PrometheusOpts.Password)
 		})
 	}
 }
diff --git a/pkg/domain/podman/update/common_test.go b/pkg/domain/podman/update/common_test.go
new file mode 100644
index 00000000..b7503fd9
--- /dev/null
+++ b/pkg/domain/podman/update/common_test.go
@@ -0,0 +1,305 @@
+//go:build podman
+// +build podman
+
+package update
+
+import (
+	_ "embed"
+	"flag"
+	"os"
+	"regexp"
+	"strings"
+	"testing"
+	"time"
+
+	"github.com/google/uuid"
+	"github.com/skupperproject/skupper/client/container"
+	"github.com/skupperproject/skupper/client/podman"
+	domainpodman "github.com/skupperproject/skupper/pkg/domain/podman"
+	"github.com/skupperproject/skupper/pkg/images"
+	"gotest.tools/assert"
+)
+
+// some podman tests require a cluster as well
+var clusterRun = flag.Bool("use-cluster", false, "run tests against a configured cluster")
+
+func TestMain(m *testing.M) {
+	flag.Parse()
+	os.Exit(m.Run())
+}
+
+var (
+	//go:embed skrouterd.json
+	skrouterdJson string
+	//go:embed skupper-services.json
+	skupperServicesJson string
+)
+
+func TestContainerImagesTask(t *testing.T) {
+	cli := podman.NewPodmanClientMock(mockContainers())
+	mock := cli.RestClient.(*podman.RestClientMock)
+	assert.Assert(t, mock.MockVolumeFiles(mockVolumes()))
+	defer func() {
+		_ = mock.CleanupMockVolumeDir()
+	}()
+	sh := domainpodman.NewSitePodmanHandlerFromCli(cli)
+	site, err := sh.Get()
+	assert.Assert(t, err)
+	imagesTask := NewContainerImagesTask(cli)
+
+	tests := []struct {
+		name    string
+		version string
+		changed bool
+	}{
+		{
+			name:    "newer-version",
+			version: "1.5.0",
+			changed: true,
+		},
+		{
+			name:    "older-version",
+			version: "1.4.0",
+			changed: false,
+		},
+		{
+			name:    "invalid-version",
+			version: "invalid-version",
+			changed: false,
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			imagesTask.version = test.version
+			t.Logf("New version: %s - current mock site version: %s", test.version, site.GetVersion())
+			assert.Assert(t, imagesTask.AppliesTo(site.GetVersion()) == test.changed)
+			res := imagesTask.Run()
+			assert.Assert(t, len(res.Errors) == 0)
+			assert.Assert(t, res.Changed() == test.changed)
+			expectedChanges := 4
+			if !test.changed {
+				expectedChanges = 0
+			}
+			assert.Equal(t, expectedChanges, len(res.GetChanges()), "Changes performed: ", res.GetChanges())
+		})
+	}
+}
+
+func TestVersionUpdateTask(t *testing.T) {
+	cli := podman.NewPodmanClientMock(mockContainers())
+	mock := cli.RestClient.(*podman.RestClientMock)
+	assert.Assert(t, mock.MockVolumeFiles(mockVolumes()))
+	defer func() {
+		_ = mock.CleanupMockVolumeDir()
+	}()
+	ch := domainpodman.NewRouterConfigHandlerPodman(cli)
+
+	versionUpdTask := NewVersionUpdateTask(cli)
+	tests := []struct {
+		name       string
+		curVersion string
+		version    string
+		changed    bool
+	}{
+		{
+			name:       "newer-version",
+			curVersion: "1.4.2",
+			version:    "1.5.0",
+			changed:    true,
+		},
+		{
+			name:       "older-version",
+			curVersion: "1.4.2",
+			version:    "1.4.0",
+			changed:    false,
+		},
+		{
+			name:       "invalid-old-version",
+			curVersion: "invalid-old-version",
+			version:    "1.5.0",
+			changed:    false,
+		},
+		{
+			name:       "invalid-new-version",
+			curVersion: "1.4.2",
+			version:    "invalid-new-version",
+			changed:    false,
+		},
+		{
+			name:       "invalid-versions",
+			curVersion: "invalid-old-version",
+			version:    "invalid-new-version",
+			changed:    false,
+		},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			// updating site version to curVersion
+			cfg, err := ch.GetRouterConfig()
+			assert.Assert(t, err)
+			siteMeta := cfg.GetSiteMetadata()
+			siteMeta.Version = test.curVersion
+			cfg.SetSiteMetadata(&siteMeta)
+			assert.Assert(t, ch.SaveRouterConfig(cfg))
+			// simulating new version
+			versionUpdTask.version = test.version
+			t.Logf("New version: %s - current mock site version: %s", test.version, test.curVersion)
+
+			// validating if task has to be executed or not
+			assert.Assert(t, versionUpdTask.AppliesTo(test.curVersion) == test.changed)
+
+			// mocking task execution
+			if test.changed {
+				res := versionUpdTask.Run()
+				assert.Assert(t, len(res.Errors) == 0)
+				assert.Assert(t, res.Changed() == test.changed)
+				assert.Assert(t, 1 == len(res.GetChanges()))
+			}
+		})
+	}
+}
+
+func mockContainers() []*container.Container {
+	imageNoTag := regexp.MustCompile(`(.*):.*`)
+	return []*container.Container{
+		{
+			ID:    strings.Replace(uuid.New().String(), "-", "", -1),
+			Name:  "skupper-router",
+			Image: imageNoTag.ReplaceAllString(images.GetRouterImageName(), "$1:old"),
+			Labels: map[string]string{
+				"application":          "skupper",
+				"skupper.io/component": "skupper-router",
+			},
+			Networks: map[string]container.ContainerNetworkInfo{
+				"skupper": {
+					ID:        "skupper",
+					IPAddress: "172.17.0.10",
+					Gateway:   "172.17.0.1",
+					Aliases:   []string{"skupper-router"},
+					//Aliases:   []string{"skupper", "service-controller"},
+				},
+			},
+			Ports: []container.Port{
+				{Host: "55671", HostIP: "", Target: "55671", Protocol: "tcp"},
+				{Host: "45671", HostIP: "", Target: "45671", Protocol: "tcp"},
+			},
+			Running:   true,
+			CreatedAt: time.Now(),
+			StartedAt: time.Now(),
+		},
+		{
+			ID:    strings.Replace(uuid.New().String(), "-", "", -1),
+			Name:  "skupper-controller-podman",
+			Image: imageNoTag.ReplaceAllString(images.GetControllerPodmanImageName(), "$1:old"),
+			Labels: map[string]string{
+				"application":          "skupper",
+				"skupper.io/component": "skupper-controller-podman",
+			},
+			Networks: map[string]container.ContainerNetworkInfo{
+				"skupper": {
+					ID:        "skupper",
+					IPAddress: "172.17.0.11",
+					Gateway:   "172.17.0.1",
+					Aliases:   []string{"skupper", "service-controller-podman"},
+				},
+			},
+			Running:   true,
+			CreatedAt: time.Now(),
+			StartedAt: time.Now(),
+		}, {
+			ID:    strings.Replace(uuid.New().String(), "-", "", -1),
+			Name:  "flow-collector",
+			Image: imageNoTag.ReplaceAllString(images.GetFlowCollectorImageName(), "$1:old"),
+			Labels: map[string]string{
+				"application":          "skupper",
+				"skupper.io/component": "flow-collector",
+			},
+			Networks: map[string]container.ContainerNetworkInfo{
+				"skupper": {
+					ID:        "skupper",
+					IPAddress: "172.17.0.12",
+					Gateway:   "172.17.0.1",
+					Aliases:   []string{"flow-collector"},
+				},
+			},
+			Ports: []container.Port{
+				{Host: "8010", HostIP: "", Target: "8010", Protocol: "tcp"},
+			},
+			Running:   true,
+			CreatedAt: time.Now(),
+			StartedAt: time.Now(),
+		}, {
+			ID:    strings.Replace(uuid.New().String(), "-", "", -1),
+			Name:  "nginx-service",
+			Image: imageNoTag.ReplaceAllString(images.GetRouterImageName(), "$1:old"),
+			Labels: map[string]string{
+				"application":        "skupper",
+				"skupper.io/address": "nginx",
+			},
+			Networks: map[string]container.ContainerNetworkInfo{
+				"skupper": {
+					ID:        "skupper",
+					IPAddress: "172.17.0.13",
+					Gateway:   "172.17.0.1",
+					Aliases:   []string{"nginx-service"},
+				},
+			},
+			Ports: []container.Port{
+				{Host: "8080", HostIP: "", Target: "8080", Protocol: "tcp"},
+			},
+			Running:   true,
+			CreatedAt: time.Now(),
+			StartedAt: time.Now(),
+		}, {
+			ID:     strings.Replace(uuid.New().String(), "-", "", -1),
+			Name:   "nginx",
+			Image:  "docker.io/nginxinc/nginx-unprivileged:stable-alpine",
+			Labels: map[string]string{},
+			Networks: map[string]container.ContainerNetworkInfo{
+				"skupper": {
+					ID:        "skupper",
+					IPAddress: "172.17.0.14",
+					Gateway:   "172.17.0.1",
+					Aliases:   []string{"nginx"},
+				},
+			},
+			Running:   true,
+			CreatedAt: time.Now(),
+			StartedAt: time.Now(),
+		},
+	}
+}
+
+func mockVolumes() (map[string]*container.Volume, map[string]map[string]string) {
+	var volumes = map[string]*container.Volume{}
+	var volumesFiles = map[string]map[string]string{}
+	addSkupperVolume := func(name string) {
+		volumes[name] = &container.Volume{Name: name, Labels: map[string]string{"application": "skupper"}}
+	}
+	addSkupperVolume("skupper-console-certs")
+	addSkupperVolume("skupper-console-users")
+	addSkupperVolume("skupper-internal")
+	addSkupperVolume("skupper-local-ca")
+	addSkupperVolume("skupper-local-client")
+	addSkupperVolume("skupper-local-server")
+	addSkupperVolume("skupper-router-certs")
+	addSkupperVolume("skupper-service-ca")
+	addSkupperVolume("skupper-service-client")
+	addSkupperVolume("skupper-services")
+	addSkupperVolume("skupper-site-ca")
+	addSkupperVolume("skupper-site-server")
+
+	// volumes content
+	volumesFiles["skupper-internal"] = map[string]string{
+		"skrouterd.json": skrouterdJson,
+	}
+	volumesFiles["skupper-console-users"] = map[string]string{
+		"admin": "admin",
+	}
+	volumesFiles["skupper-services"] = map[string]string{
+		"skupper-services.json": skupperServicesJson,
+	}
+
+	// defining skupper-services configmap
+	return volumes, volumesFiles
+}
diff --git a/pkg/domain/update_test.go b/pkg/domain/update_test.go
new file mode 100644
index 00000000..0a7f5dbf
--- /dev/null
+++ b/pkg/domain/update_test.go
@@ -0,0 +1,452 @@
+package domain
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/skupperproject/skupper/pkg/utils"
+	"gotest.tools/assert"
+)
+
+func TestUpdateProcessor(t *testing.T) {
+	tests := []struct {
+		name            string
+		curVersion      string
+		newVersion      string
+		tasks           []UpdateTask
+		expectedChanges []string
+		expectError     bool
+	}{
+		{
+			name:       "all-tasks-and-post-tasks",
+			curVersion: "1.4.2",
+			newVersion: "1.5.0",
+			tasks: []UpdateTask{
+				NewMockUpdateTask(
+					"create new container",
+					"1.5.0",
+					func(siteVersion string) bool {
+						return utils.LessRecentThanVersion(siteVersion, "1.5.0")
+					},
+					PriorityNormal,
+					&UpdateResult{
+						changes: []string{
+							"created new container",
+						},
+						PostTasks: []UpdateTask{
+							NewMockUpdateTask(
+								"start new container",
+								"1.5.0",
+								func(siteVersion string) bool {
+									return true
+								},
+								PriorityLast,
+								&UpdateResult{
+									changes: []string{
+										"started new container",
+									},
+								},
+							),
+							NewMockUpdateTask(
+								"create config file",
+								"1.5.0",
+								func(siteVersion string) bool {
+									return true
+								},
+								PriorityFirst,
+								&UpdateResult{
+									changes: []string{
+										"created config file",
+									},
+								},
+							),
+							NewMockUpdateTask(
+								"modify config file",
+								"1.5.0",
+								func(siteVersion string) bool {
+									return true
+								},
+								PriorityFirst,
+								&UpdateResult{
+									changes: []string{
+										"modified config file",
+									},
+								},
+							),
+						},
+					},
+				),
+				NewMockUpdateTask(
+					"create new volume",
+					"1.5.0",
+					func(siteVersion string) bool {
+						return utils.LessRecentThanVersion(siteVersion, "1.5.0")
+					},
+					PriorityFirst,
+					&UpdateResult{
+						changes: []string{
+							"created new volume",
+						},
+						PostTasks: []UpdateTask{},
+					},
+				),
+				NewMockUpdateTask(
+					"this task should not be executed",
+					"1.4.0",
+					func(siteVersion string) bool {
+						return utils.LessRecentThanVersion(siteVersion, "1.4.0")
+					},
+					PriorityFirst,
+					&UpdateResult{
+						changes: []string{
+							"this task was incorrectly executed",
+						},
+						PostTasks: []UpdateTask{
+							NewMockUpdateTask(
+								"this post task should not be executed",
+								"1.4.0",
+								func(siteVersion string) bool {
+									return false
+								},
+								PriorityFirst,
+								&UpdateResult{
+									changes: []string{
+										"this post task was incorrectly executed",
+									},
+								},
+							),
+						},
+					},
+				),
+			},
+			expectedChanges: []string{
+				"created new volume",
+				"created new container",
+				"created config file",
+				"modified config file",
+				"started new container",
+			},
+			expectError: false,
+		},
+		{
+			name:       "second-task-with-warning",
+			curVersion: "1.4.2",
+			newVersion: "1.5.0",
+			tasks: []UpdateTask{
+				NewMockUpdateTask(
+					"create new container",
+					"1.5.0",
+					func(siteVersion string) bool {
+						return utils.LessRecentThanVersion(siteVersion, "1.5.0")
+					},
+					PriorityNormal,
+					&UpdateResult{
+						warnings: []string{
+							"container already exists, ignoring",
+						},
+						changes: []string{
+							"created new container",
+						},
+						PostTasks: []UpdateTask{
+							NewMockUpdateTask(
+								"start new container",
+								"1.5.0",
+								func(siteVersion string) bool {
+									return true
+								},
+								PriorityLast,
+								&UpdateResult{
+									changes: []string{
+										"started new container",
+									},
+								},
+							),
+						},
+					},
+				),
+				NewMockUpdateTask(
+					"create new volume",
+					"1.5.0",
+					func(siteVersion string) bool {
+						return utils.LessRecentThanVersion(siteVersion, "1.5.0")
+					},
+					PriorityFirst,
+					&UpdateResult{
+						changes: []string{
+							"created new volume",
+						},
+						PostTasks: []UpdateTask{},
+					},
+				),
+			},
+			expectedChanges: []string{
+				"created new volume",
+				"created new container",
+				"started new container",
+			},
+			expectError: false,
+		},
+		{
+			name:       "second-task-with-error",
+			curVersion: "1.4.2",
+			newVersion: "1.5.0",
+			tasks: []UpdateTask{
+				NewMockUpdateTask(
+					"create new container",
+					"1.5.0",
+					func(siteVersion string) bool {
+						return utils.LessRecentThanVersion(siteVersion, "1.5.0")
+					},
+					PriorityNormal,
+					&UpdateResult{
+						Errors: []error{
+							fmt.Errorf("container already exists"),
+						},
+						PostTasks: []UpdateTask{
+							NewMockUpdateTask(
+								"start new container",
+								"1.5.0",
+								func(siteVersion string) bool {
+									return true
+								},
+								PriorityLast,
+								&UpdateResult{
+									changes: []string{
+										"should not be executed - started new container",
+									},
+								},
+							),
+						},
+					},
+				),
+				NewMockUpdateTask(
+					"create new volume",
+					"1.5.0",
+					func(siteVersion string) bool {
+						return utils.LessRecentThanVersion(siteVersion, "1.5.0")
+					},
+					PriorityFirst,
+					&UpdateResult{
+						changes: []string{
+							"created new volume",
+						},
+						PostTasks: []UpdateTask{},
+					},
+				),
+			},
+			expectedChanges: []string{
+				"created new volume",
+			},
+			expectError: true,
+		},
+		{
+			name:       "second-post-task-with-error",
+			curVersion: "1.4.2",
+			newVersion: "1.5.0",
+			tasks: []UpdateTask{
+				NewMockUpdateTask(
+					"create new container",
+					"1.5.0",
+					func(siteVersion string) bool {
+						return utils.LessRecentThanVersion(siteVersion, "1.5.0")
+					},
+					PriorityNormal,
+					&UpdateResult{
+						changes: []string{
+							"created new container",
+						},
+						PostTasks: []UpdateTask{
+							NewMockUpdateTask(
+								"start new container",
+								"1.5.0",
+								func(siteVersion string) bool {
+									return true
+								},
+								PriorityLast,
+								&UpdateResult{
+									changes: []string{
+										"started new container",
+									},
+								},
+							),
+							NewMockUpdateTask(
+								"create config file",
+								"1.5.0",
+								func(siteVersion string) bool {
+									return true
+								},
+								PriorityFirst,
+								&UpdateResult{
+									changes: []string{
+										"created config file",
+									},
+								},
+							),
+							NewMockUpdateTask(
+								"modify config file",
+								"1.5.0",
+								func(siteVersion string) bool {
+									return true
+								},
+								PriorityFirst,
+								&UpdateResult{
+									Errors: []error{
+										fmt.Errorf("error modifying file"),
+									},
+								},
+							),
+						},
+					},
+				),
+				NewMockUpdateTask(
+					"create new volume",
+					"1.5.0",
+					func(siteVersion string) bool {
+						return utils.LessRecentThanVersion(siteVersion, "1.5.0")
+					},
+					PriorityFirst,
+					&UpdateResult{
+						changes: []string{
+							"created new volume",
+						},
+						PostTasks: []UpdateTask{},
+					},
+				),
+				NewMockUpdateTask(
+					"this task should not be executed",
+					"1.4.0",
+					func(siteVersion string) bool {
+						return utils.LessRecentThanVersion(siteVersion, "1.4.0")
+					},
+					PriorityFirst,
+					&UpdateResult{
+						changes: []string{
+							"this task was incorrectly executed",
+						},
+						PostTasks: []UpdateTask{
+							NewMockUpdateTask(
+								"this post task should not be executed",
+								"1.4.0",
+								func(siteVersion string) bool {
+									return false
+								},
+								PriorityFirst,
+								&UpdateResult{
+									changes: []string{
+										"this post task was incorrectly executed",
+									},
+								},
+							),
+						},
+					},
+				),
+			},
+			expectedChanges: []string{
+				"created new volume",
+				"created new container",
+				"created config file",
+			},
+			expectError: true,
+		},
+		{
+			name:       "no-updated-needed",
+			curVersion: "1.5.0",
+			newVersion: "1.5.0",
+			tasks: []UpdateTask{
+				NewMockUpdateTask(
+					"create new container",
+					"1.5.0",
+					func(siteVersion string) bool {
+						return utils.LessRecentThanVersion(siteVersion, "1.5.0")
+					},
+					PriorityNormal,
+					&UpdateResult{
+						changes: []string{
+							"created new container",
+						},
+						PostTasks: []UpdateTask{
+							NewMockUpdateTask(
+								"start new container",
+								"1.5.0",
+								func(siteVersion string) bool {
+									return true
+								},
+								PriorityLast,
+								&UpdateResult{
+									changes: []string{
+										"started new container",
+									},
+								},
+							),
+						},
+					},
+				),
+				NewMockUpdateTask(
+					"create new volume",
+					"1.5.0",
+					func(siteVersion string) bool {
+						return utils.LessRecentThanVersion(siteVersion, "1.5.0")
+					},
+					PriorityFirst,
+					&UpdateResult{
+						changes: []string{
+							"created new volume",
+						},
+						PostTasks: []UpdateTask{},
+					},
+				),
+			},
+			expectError: false,
+		},
+	}
+
+	for _, scenario := range tests {
+		t.Run(scenario.name, func(t *testing.T) {
+			up := &UpdateProcessor{
+				Verbose: true,
+			}
+			up.RegisterTasks(scenario.tasks...)
+			err := up.Process(scenario.curVersion)
+			assert.Equal(t, err != nil, scenario.expectError, fmt.Sprintf("error expected == %v but got err == %v", scenario.expectError, err))
+			assert.DeepEqual(t, scenario.expectedChanges, up.changes)
+		})
+	}
+}
+
+func NewMockUpdateTask(info, version string, appliesTo func(siteVersion string) bool, priority UpdatePriority, result *UpdateResult) UpdateTask {
+	return &mockUpdateTask{
+		info:      info,
+		version:   version,
+		appliesTo: appliesTo,
+		priority:  priority,
+		result:    result,
+	}
+}
+
+type mockUpdateTask struct {
+	info      string
+	version   string
+	appliesTo func(siteVersion string) bool
+	priority  UpdatePriority
+	result    *UpdateResult
+}
+
+func (u *mockUpdateTask) Info() string {
+	return u.info
+}
+
+func (u *mockUpdateTask) AppliesTo(siteVersion string) bool {
+	return u.appliesTo(siteVersion)
+}
+
+func (u *mockUpdateTask) Version() string {
+	return u.version
+}
+
+func (u *mockUpdateTask) Priority() UpdatePriority {
+	return u.priority
+}
+
+func (u *mockUpdateTask) Run() *UpdateResult {
+	return u.result
+}
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("", "")