Repository: skupperproject/skupper
Pull request #1201 (2023-08-11 13:03:11)

add regex match


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 834a9cfa..a0ad1ec9 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -106,7 +106,6 @@ commands:
       - go/install:
           version: "1.19"
       - kube-orb/install-kubectl
-      - run: make generate-client
       - run: make
 
   local_registry_start:
@@ -378,10 +377,19 @@ workflows:
           context:
             - skupper-org
 
+      - generate-manifest:
+          <<: *run_for_numeric_tags
+          <<: *release_requires
+          requires:
+            - publish-github-release-images
+          context:
+            - skupper-org
+
       - publish-github-release-artifacts:
           <<: *run_for_numeric_tags
           <<: *release_requires
           requires:
+            - generate-manifest
             - publish-github-release-images
           context:
             - skupper-org
@@ -408,7 +416,6 @@ jobs:
                 exit 1
               fi
       - go/mod-download-cached
-      - run: make generate-client
       - run: make
       - run: make package
       - run: make clean
@@ -420,7 +427,6 @@ jobs:
     steps:
       - docker/install-docker
       - checkout
-      - run: make -e generate-client
       - run: make -e docker-build
       - run:
           name: persisting images to workspace
@@ -491,15 +497,10 @@ jobs:
           goos: linux
           goarch: arm64
           platform: linux-arm64
-      - setup_remote_docker
-      - run: make generate-manifest
-      - run: mkdir skupper-manifest
-      - run: cp ./manifest.json skupper-manifest
       - persist_to_workspace:
           root: .
           paths:
             - dist
-            - skupper-manifest
 
   main_tests_minikube_local_cluster:
     executor: local_cluster_test_executor
@@ -530,6 +531,22 @@ jobs:
       - run: kubectl cluster-info
       - run_cluster_policy_tests
 
+  generate-manifest:
+    executor:
+      name: go/default
+      tag: "1.19"
+    steps:
+      - checkout
+      - go/mod-download-cached
+      - setup_remote_docker
+      - run: make generate-manifest
+      - run: mkdir skupper-manifest
+      - run: cp ./manifest.json skupper-manifest
+      - persist_to_workspace:
+          root: .
+          paths:
+            - skupper-manifest
+
   publish-github-release-artifacts:
     docker:
       - image: cibuilds/github:0.10
@@ -572,7 +589,6 @@ jobs:
             echo 'export FLOW_COLLECTOR_IMAGE=quay.io/skupper/flow-collector:${CIRCLE_TAG}' >> $BASH_ENV
             echo 'export TEST_IMAGE=quay.io/skupper/skupper-tests:${CIRCLE_TAG}' >> $BASH_ENV
             source $BASH_ENV
-            make -e generate-client
             make -e docker-build
             make -e docker-push
 
@@ -594,6 +610,5 @@ jobs:
             echo 'export FLOW_COLLECTOR_IMAGE=quay.io/skupper/flow-collector:main' >> $BASH_ENV
             echo 'export TEST_IMAGE=quay.io/skupper/skupper-tests:main' >> $BASH_ENV
             source $BASH_ENV
-            make -e generate-client
             make -e docker-build
             make -e docker-push
diff --git a/Makefile b/Makefile
index 0148783e..8b11d8ba 100644
--- a/Makefile
+++ b/Makefile
@@ -9,7 +9,7 @@ TEST_BINARIES_FOLDER := ${PWD}/test/integration/bin
 DOCKER := docker
 LDFLAGS := -X github.com/skupperproject/skupper/pkg/version.Version=${VERSION}
 
-all: build-cmd build-get build-config-sync build-controllers build-tests build-manifest
+all: generate-client build-cmd build-get build-config-sync build-controllers build-tests build-manifest
 
 build-tests:
 	mkdir -p ${TEST_BINARIES_FOLDER}
@@ -20,7 +20,7 @@ build-tests:
 	go test -c -tags=job -v ./test/integration/examples/custom/hipstershop/job -o ${TEST_BINARIES_FOLDER}/grpcclient_test
 	go test -c -tags=job -v ./test/integration/examples/tls_t/job -o ${TEST_BINARIES_FOLDER}/tls_test
 
-build-cmd:
+build-cmd: generate-client
 	go build -ldflags="${LDFLAGS}"  -o skupper ./cmd/skupper
 
 build-get:
@@ -49,7 +49,7 @@ build-manifest:
 docker-build-test-image:
 	${DOCKER} build -t ${TEST_IMAGE} -f Dockerfile.ci-test .
 
-docker-build: docker-build-test-image
+docker-build: generate-client docker-build-test-image
 	${DOCKER} build -t ${SERVICE_CONTROLLER_IMAGE} -f Dockerfile.service-controller .
 	${DOCKER} build -t ${CONTROLLER_PODMAN_IMAGE} -f Dockerfile.controller-podman .
 	${DOCKER} build -t ${SITE_CONTROLLER_IMAGE} -f Dockerfile.site-controller .
diff --git a/api/types/client.go b/api/types/client.go
index 75b00195..1a88e627 100644
--- a/api/types/client.go
+++ b/api/types/client.go
@@ -65,6 +65,7 @@ type RouterOptions struct {
 	DataConnectionCount string
 	IngressHost         string
 	ServiceAnnotations  map[string]string
+	PodAnnotations      map[string]string
 	LoadBalancerIp      string
 	DisableMutualTLS    bool
 }
@@ -73,6 +74,7 @@ type ControllerOptions struct {
 	Tuning
 	IngressHost        string
 	ServiceAnnotations map[string]string
+	PodAnnotations     map[string]string
 	LoadBalancerIp     string
 }
 
@@ -91,6 +93,7 @@ type PrometheusServerOptions struct {
 	AuthMode       string
 	User           string
 	Password       string
+	PodAnnotations map[string]string
 }
 
 type SiteConfigSpec struct {
diff --git a/client/router_create.go b/client/router_create.go
index 2e9b0a0f..74e682c6 100644
--- a/client/router_create.go
+++ b/client/router_create.go
@@ -37,9 +37,11 @@ func getServingSecretAnnotations(name string) map[string]string {
 }
 
 func OauthProxyContainer(serviceAccount string, servicePort string) *corev1.Container {
+	image := images.GetOauthProxyImageDetails()
 	return &corev1.Container{
-		Image: "openshift/oauth-proxy:latest",
-		Name:  "oauth-proxy",
+		Image:           image.Name,
+		ImagePullPolicy: kube.GetPullPolicy(image.PullPolicy),
+		Name:            "oauth-proxy",
 		Args: []string{
 			"--https-address=:" + strconv.Itoa(int(types.ConsoleOpenShiftOauthServiceTargetPort)),
 			"--provider=openshift",
@@ -121,7 +123,13 @@ func (cli *VanClient) GetVanPrometheusServerSpec(options types.SiteConfigSpec, v
 	for key, value := range van.PrometheusServer.LabelSelector {
 		van.PrometheusServer.Labels[key] = value
 	}
-	van.PrometheusServer.Annotations = options.Annotations
+	van.PrometheusServer.Annotations = map[string]string{}
+	for key, value := range options.Annotations {
+		van.PrometheusServer.Annotations[key] = value
+	}
+	for key, value := range options.PrometheusServer.PodAnnotations {
+		van.PrometheusServer.Annotations[key] = value
+	}
 	for key, value := range options.Labels {
 		van.PrometheusServer.Labels[key] = value
 	}
@@ -256,7 +264,13 @@ func (cli *VanClient) GetVanControllerSpec(options types.SiteConfigSpec, van *ty
 	for key, value := range van.Controller.LabelSelector {
 		van.Controller.Labels[key] = value
 	}
-	van.Controller.Annotations = options.Annotations
+	van.Controller.Annotations = map[string]string{}
+	for key, value := range options.Annotations {
+		van.Controller.Annotations[key] = value
+	}
+	for key, value := range options.Controller.PodAnnotations {
+		van.Controller.Annotations[key] = value
+	}
 	for key, value := range options.Labels {
 		van.Controller.Labels[key] = value
 	}
@@ -684,6 +698,9 @@ func (cli *VanClient) GetRouterSpecFromOpts(options types.SiteConfigSpec, siteId
 	for key, value := range options.Annotations {
 		van.Transport.Annotations[key] = value
 	}
+	for key, value := range options.Router.PodAnnotations {
+		van.Transport.Annotations[key] = value
+	}
 	runAsNonRoot := true
 	van.Transport.SecurityContext = &corev1.SecurityContext{
 		RunAsNonRoot: &runAsNonRoot,
diff --git a/client/router_update.go b/client/router_update.go
index 12059e8d..c08ca63b 100644
--- a/client/router_update.go
+++ b/client/router_update.go
@@ -1041,10 +1041,19 @@ func (cli *VanClient) RouterUpdateAnnotations(ctx context.Context, settings *cor
 	if err != nil {
 		return false, err
 	}
-	updated, err := cli.updateAnnotationsOnDeployment(ctx, settings.ObjectMeta.Namespace, types.ControllerDeploymentName, siteConfig.Spec.Annotations)
+
+	controllerAnnotations := map[string]string{}
+	for key, value := range siteConfig.Spec.Annotations {
+		controllerAnnotations[key] = value
+	}
+	for key, value := range siteConfig.Spec.Controller.PodAnnotations {
+		controllerAnnotations[key] = value
+	}
+	updated, err := cli.updateAnnotationsOnDeployment(ctx, settings.ObjectMeta.Namespace, types.ControllerDeploymentName, controllerAnnotations)
 	if err != nil {
 		return updated, err
 	}
+
 	transportAnnotations := map[string]string{}
 	for key, value := range types.TransportPrometheusAnnotations {
 		transportAnnotations[key] = value
@@ -1052,10 +1061,26 @@ func (cli *VanClient) RouterUpdateAnnotations(ctx context.Context, settings *cor
 	for key, value := range siteConfig.Spec.Annotations {
 		transportAnnotations[key] = value
 	}
+	for key, value := range siteConfig.Spec.Router.PodAnnotations {
+		transportAnnotations[key] = value
+	}
 	updated, err = cli.updateAnnotationsOnDeployment(ctx, settings.ObjectMeta.Namespace, types.TransportDeploymentName, transportAnnotations)
 	if err != nil {
 		return updated, err
 	}
+
+	prometheusAnnotations := map[string]string{}
+	for key, value := range siteConfig.Spec.Annotations {
+		prometheusAnnotations[key] = value
+	}
+	for key, value := range siteConfig.Spec.PrometheusServer.PodAnnotations {
+		prometheusAnnotations[key] = value
+	}
+	updated, err = cli.updateAnnotationsOnDeployment(ctx, settings.ObjectMeta.Namespace, types.PrometheusDeploymentName, prometheusAnnotations)
+	if err != nil {
+		return updated, err
+	}
+
 	return updated, nil
 }
 
diff --git a/client/serviceinterface_update.go b/client/serviceinterface_update.go
index 336cbf19..a0461e77 100644
--- a/client/serviceinterface_update.go
+++ b/client/serviceinterface_update.go
@@ -329,7 +329,9 @@ func removeServiceInterfaceTarget(serviceName string, targetName string, deleteI
 			modified := false
 			targets := []types.ServiceInterfaceTarget{}
 			for _, t := range service.Targets {
-				if (t.Name == targetName || (t.Name == "" && targetName == serviceName)) && (t.Namespace == "" || t.Namespace == namespace) {
+				targetFound := (t.Name == targetName && t.Service == "") && t.Namespace == namespace
+				targetServiceFound := t.Service == targetName && t.Namespace == ""
+				if targetFound || targetServiceFound {
 					modified = true
 				} else {
 					targets = append(targets, t)
diff --git a/cmd/controller-podman/main.go b/cmd/controller-podman/main.go
index 0217ce0e..f961d191 100644
--- a/cmd/controller-podman/main.go
+++ b/cmd/controller-podman/main.go
@@ -35,10 +35,7 @@ func main() {
 	// set up signals so we handle the first shutdown signal gracefully
 	stopCh := SetupSignalHandler()
 
-	tlsConfig, err := certs.GetTlsConfig(true, types.ControllerConfigPath+"tls.crt", types.ControllerConfigPath+"tls.key", types.ControllerConfigPath+"ca.crt")
-	if err != nil {
-		log.Fatal("Error getting tls config", err.Error())
-	}
+	tlsConfig := certs.GetTlsConfigRetriever(true, types.ControllerConfigPath+"tls.crt", types.ControllerConfigPath+"tls.key", types.ControllerConfigPath+"ca.crt")
 
 	controller, err := podmancontroller.NewControllerPodman(origin, tlsConfig)
 	if err != nil {
diff --git a/cmd/flow-collector/controller.go b/cmd/flow-collector/controller.go
index 595413e5..85524887 100644
--- a/cmd/flow-collector/controller.go
+++ b/cmd/flow-collector/controller.go
@@ -1,7 +1,6 @@
 package main
 
 import (
-	"crypto/tls"
 	"log"
 	"time"
 
@@ -9,6 +8,7 @@ import (
 
 	"github.com/interconnectedcloud/go-amqp"
 	"github.com/prometheus/client_golang/prometheus"
+	"github.com/skupperproject/skupper/pkg/certs"
 	"github.com/skupperproject/skupper/pkg/flow"
 	"github.com/skupperproject/skupper/pkg/qdr"
 )
@@ -20,7 +20,7 @@ type Controller struct {
 	FlowCollector *flow.FlowCollector
 }
 
-func NewController(origin string, reg prometheus.Registerer, scheme string, host string, port string, tlsConfig *tls.Config, recordTtl time.Duration) (*Controller, error) {
+func NewController(origin string, reg prometheus.Registerer, scheme string, host string, port string, tlsConfig *certs.TlsConfigRetriever, recordTtl time.Duration) (*Controller, error) {
 
 	controller := &Controller{
 		origin:        origin,
diff --git a/cmd/flow-collector/main.go b/cmd/flow-collector/main.go
index 2fc5e875..33f2c25f 100644
--- a/cmd/flow-collector/main.go
+++ b/cmd/flow-collector/main.go
@@ -208,10 +208,7 @@ func main() {
 		prometheusUrl = os.Getenv("PROMETHEUS_URL")
 	}
 
-	tlsConfig, err := certs.GetTlsConfig(true, types.ControllerConfigPath+"tls.crt", types.ControllerConfigPath+"tls.key", types.ControllerConfigPath+"ca.crt")
-	if err != nil {
-		log.Fatal("Error getting tls config", err.Error())
-	}
+	tlsConfig := certs.GetTlsConfigRetriever(true, types.ControllerConfigPath+"tls.crt", types.ControllerConfigPath+"tls.key", types.ControllerConfigPath+"ca.crt")
 
 	conn, err := getConnectInfo(types.ControllerConfigPath + "connect.json")
 	if err != nil {
diff --git a/cmd/manifest/manifest.go b/cmd/manifest/manifest.go
index 2719a940..fb332039 100644
--- a/cmd/manifest/manifest.go
+++ b/cmd/manifest/manifest.go
@@ -3,9 +3,10 @@ package main
 import (
 	"encoding/json"
 	"fmt"
+	"os"
+
 	"github.com/skupperproject/skupper/pkg/images"
 	"github.com/spf13/cobra"
-	"os"
 )
 
 type SkupperImage struct {
@@ -54,6 +55,10 @@ func generateManifestFile() error {
 				Name: images.GetPrometheusServerImageName(),
 				SHA:  images.GetSha(images.GetPrometheusServerImageName()),
 			},
+			{
+				Name: images.GetOauthProxyImageName(),
+				SHA:  images.GetSha(images.GetOauthProxyImageName()),
+			},
 		},
 	}
 
diff --git a/cmd/service-controller/console_server.go b/cmd/service-controller/console_server.go
index 51702a90..b3bd2670 100644
--- a/cmd/service-controller/console_server.go
+++ b/cmd/service-controller/console_server.go
@@ -1,7 +1,6 @@
 package main
 
 import (
-	"crypto/tls"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -20,6 +19,7 @@ import (
 
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/client"
+	"github.com/skupperproject/skupper/pkg/certs"
 	"github.com/skupperproject/skupper/pkg/data"
 	"github.com/skupperproject/skupper/pkg/event"
 	"github.com/skupperproject/skupper/pkg/qdr"
@@ -40,7 +40,7 @@ type ConsoleServer struct {
 	accessRevoker *AccessRevoker
 }
 
-func newConsoleServer(cli *client.VanClient, config *tls.Config, eventHandler event.EventHandlerInterface) *ConsoleServer {
+func newConsoleServer(cli *client.VanClient, config *certs.TlsConfigRetriever, eventHandler event.EventHandlerInterface) *ConsoleServer {
 	pool := qdr.NewAgentPool("amqps://"+types.QualifiedServiceName(types.LocalTransportServiceName, cli.Namespace)+":5671", config)
 	return &ConsoleServer{
 		agentPool:     pool,
diff --git a/cmd/service-controller/controller.go b/cmd/service-controller/controller.go
index dfaf897d..c5bd9215 100644
--- a/cmd/service-controller/controller.go
+++ b/cmd/service-controller/controller.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"context"
-	"crypto/tls"
 	jsonencoding "encoding/json"
 	"fmt"
 	"log"
@@ -30,6 +29,7 @@ import (
 
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/client"
+	"github.com/skupperproject/skupper/pkg/certs"
 	"github.com/skupperproject/skupper/pkg/event"
 	"github.com/skupperproject/skupper/pkg/flow"
 	"github.com/skupperproject/skupper/pkg/kube"
@@ -57,7 +57,6 @@ type Controller struct {
 
 	// service_sync state:
 	disableServiceSync bool
-	tlsConfig          *tls.Config
 	amqpClient         *amqp.Client
 	amqpSession        *amqp.Session
 	byOrigin           map[string]map[string]types.ServiceInterface
@@ -116,7 +115,7 @@ func hasOriginalAnnotations(service corev1.Service) bool {
 	return hasOriginalSelector(service) || hasOriginalTargetPort(service) || hasOriginalAssigned(service)
 }
 
-func NewController(cli *client.VanClient, origin string, tlsConfig *tls.Config, disableServiceSync bool) (*Controller, error) {
+func NewController(cli *client.VanClient, origin string, tlsConfig *certs.TlsConfigRetriever, disableServiceSync bool) (*Controller, error) {
 
 	// create informers
 	svcInformer := corev1informer.NewServiceInformer(
@@ -163,7 +162,6 @@ func NewController(cli *client.VanClient, origin string, tlsConfig *tls.Config,
 		vanClient:          cli,
 		policy:             client.NewClusterPolicyValidator(cli),
 		origin:             origin,
-		tlsConfig:          tlsConfig,
 		bridgeDefInformer:  bridgeDefInformer,
 		svcDefInformer:     svcDefInformer,
 		svcInformer:        svcInformer,
@@ -661,7 +659,26 @@ func (c *Controller) updateServiceSync(defs *corev1.ConfigMap) {
 }
 
 func (c *Controller) deleteHeadlessProxy(statefulset *appsv1.StatefulSet) error {
-	return c.vanClient.KubeClient.AppsV1().StatefulSets(c.vanClient.Namespace).Delete(context.TODO(), statefulset.ObjectMeta.Name, metav1.DeleteOptions{})
+	err := c.vanClient.KubeClient.AppsV1().StatefulSets(c.vanClient.Namespace).Delete(context.TODO(), statefulset.ObjectMeta.Name, metav1.DeleteOptions{})
+	if err != nil {
+		c.eventHandler.RecordWarningEvent(ServiceControllerDeleteEvent, fmt.Sprintf("Error deleting headless proxy %q: %s", statefulset.Name, err))
+		return err
+	}
+	c.eventHandler.RecordNormalEvent(ServiceControllerDeleteEvent, fmt.Sprintf("Deleted headless proxy %q", statefulset.Name))
+	svcName := statefulset.Spec.ServiceName
+	svc, found, err := c.GetService(svcName)
+	if !found {
+		c.eventHandler.RecordWarningEvent(ServiceControllerDeleteEvent, fmt.Sprintf("Headless proxy service %q not found", svcName))
+	} else if err != nil {
+		c.eventHandler.RecordWarningEvent(ServiceControllerDeleteEvent, fmt.Sprintf("Error retrieving headless proxy service %q: %s", svcName, err))
+	} else if c.IsOwned(svc) {
+		if err = kube.DeleteService(svcName, c.vanClient.Namespace, c.vanClient.KubeClient); err != nil {
+			c.eventHandler.RecordWarningEvent(ServiceControllerDeleteEvent, fmt.Sprintf("Error deleting headless proxy service %q: %s", statefulset.Name, err))
+			return err
+		}
+		c.eventHandler.RecordNormalEvent(ServiceControllerDeleteEvent, fmt.Sprintf("Deleted headless proxy service %q", statefulset.Name))
+	}
+	return nil
 }
 
 func (c *Controller) ensureHeadlessProxyFor(bindings *service.ServiceBindings, statefulset *appsv1.StatefulSet) error {
diff --git a/cmd/service-controller/main.go b/cmd/service-controller/main.go
index 31c8ce51..5a06712d 100644
--- a/cmd/service-controller/main.go
+++ b/cmd/service-controller/main.go
@@ -67,10 +67,7 @@ func main() {
 		log.Fatal("Error getting van client", err.Error())
 	}
 
-	tlsConfig, err := certs.GetTlsConfig(true, types.ControllerConfigPath+"tls.crt", types.ControllerConfigPath+"tls.key", types.ControllerConfigPath+"ca.crt")
-	if err != nil {
-		log.Fatal("Error getting tls config", err.Error())
-	}
+	tlsConfig := certs.GetTlsConfigRetriever(true, types.ControllerConfigPath+"tls.crt", types.ControllerConfigPath+"tls.key", types.ControllerConfigPath+"ca.crt")
 
 	event.StartDefaultEventStore(stopCh)
 
diff --git a/cmd/service-controller/policy_controller.go b/cmd/service-controller/policy_controller.go
index 984f25fa..e17e8bab 100644
--- a/cmd/service-controller/policy_controller.go
+++ b/cmd/service-controller/policy_controller.go
@@ -354,7 +354,7 @@ func (c *PolicyController) validateExposeStateChanged() {
 			}
 			if !res.Allowed() {
 				// resource is no longer allowed, unbinding
-				event.Recordf(c.name, "[validateExposeStateChanged] exposed resource is no longer authorized - unbinding service %s: %v", service.Address, err)
+				event.Recordf(c.name, "[validateExposeStateChanged] exposed resource is no longer authorized - unbinding target %s/%s for service %s", targetType, target.Name, service.Address)
 				err = c.cli.ServiceInterfaceUnbind(context.Background(), "deployment", target.Name, service.Address, false, svcNamespace)
 				if err != nil {
 					event.Recordf(c.name, "[validateExposeStateChanged] error unbinding service %s: %v", service.Address, err)
diff --git a/cmd/service-controller/site_query.go b/cmd/service-controller/site_query.go
index 3f761cd8..755c5b9d 100644
--- a/cmd/service-controller/site_query.go
+++ b/cmd/service-controller/site_query.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"context"
-	"crypto/tls"
 	"encoding/json"
 	"fmt"
 	"os"
@@ -15,6 +14,7 @@ import (
 
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/client"
+	"github.com/skupperproject/skupper/pkg/certs"
 	"github.com/skupperproject/skupper/pkg/data"
 	"github.com/skupperproject/skupper/pkg/event"
 	"github.com/skupperproject/skupper/pkg/kube"
@@ -32,17 +32,15 @@ const (
 
 type SiteQueryServer struct {
 	client    *client.VanClient
-	tlsConfig *tls.Config
 	agentPool *qdr.AgentPool
 	server    *qdr.RequestServer
 	iplookup  *IpLookup
 	siteInfo  data.Site
 }
 
-func newSiteQueryServer(cli *client.VanClient, config *tls.Config) *SiteQueryServer {
+func newSiteQueryServer(cli *client.VanClient, config *certs.TlsConfigRetriever) *SiteQueryServer {
 	sqs := SiteQueryServer{
 		client:    cli,
-		tlsConfig: config,
 		agentPool: qdr.NewAgentPool("amqps://"+types.QualifiedServiceName(types.LocalTransportServiceName, cli.Namespace)+":5671", config),
 		iplookup:  NewIpLookup(cli, nil),
 	}
diff --git a/cmd/site-controller/README.md b/cmd/site-controller/README.md
index 71cfdaad..6a811e39 100644
--- a/cmd/site-controller/README.md
+++ b/cmd/site-controller/README.md
@@ -65,6 +65,8 @@ ConfigMaps allow you to manage a Skupper site using a ConfigMap named `skupper-s
 
 `data:router-service-annotations` - Annotations to add to skupper router service
 
+`data:router-pod-annotations` - Annotations to add to skupper router pod
+
 `data:router-load-balancer-ip` - Load balancer ip that will be used for router service, if supported by cloud provider
 
 `data:service-controller` - (true/false) Run the service controller.
@@ -89,6 +91,8 @@ ConfigMaps allow you to manage a Skupper site using a ConfigMap named `skupper-s
 
 `data:controller-service-annotations` - Annotations to add to skupper controller service
 
+`data:controller-pod-annotations` - Annotations to add to skupper controller pod
+
 `data:controller-load-balancer-ip` - Load balancer ip that will be used for controller service, if supported by cloud provider
 
 For example:
diff --git a/cmd/skupper/skupper.go b/cmd/skupper/skupper.go
index 8cdceef5..1d35ab31 100644
--- a/cmd/skupper/skupper.go
+++ b/cmd/skupper/skupper.go
@@ -558,6 +558,8 @@ func NewCmdUnexpose(skupperCli SkupperServiceClient) *cobra.Command {
 		RunE:   skupperCli.Unexpose,
 	}
 	cmd.Flags().StringVar(&unexposeAddress, "address", "", "Skupper address the target was exposed as")
+
+	skupperCli.UnexposeFlags(cmd)
 	return cmd
 }
 
diff --git a/cmd/skupper/skupper_kube.go b/cmd/skupper/skupper_kube.go
index 6fc52156..3eb90ba1 100644
--- a/cmd/skupper/skupper_kube.go
+++ b/cmd/skupper/skupper_kube.go
@@ -1,6 +1,8 @@
 package main
 
 import (
+	"fmt"
+
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/spf13/cobra"
 )
@@ -83,10 +85,13 @@ func (s *SkupperKube) Options(rootCmd *cobra.Command) {
 }
 
 type kubeInit struct {
-	annotations                  []string
-	ingressAnnotations           []string
-	routerServiceAnnotations     []string
-	controllerServiceAnnotations []string
+	annotations                    []string
+	ingressAnnotations             []string
+	routerServiceAnnotations       []string
+	routerPodAnnotations           []string
+	controllerServiceAnnotations   []string
+	controllerPodAnnotations       []string
+	prometheusServerPodAnnotations []string
 }
 
 func (s *SkupperKube) NewClient(cmd *cobra.Command, args []string) {
@@ -100,3 +105,7 @@ func (s *SkupperKube) NewClient(cmd *cobra.Command, args []string) {
 	}
 	s.Cli = NewClientHandleError(s.Namespace, s.KubeContext, s.KubeConfigPath, exitOnError)
 }
+
+func targetTypeServiceTargetNamespaceError() error {
+	return fmt.Errorf("--target-namespace cannot be used when target type is service (qualify the target service name instead)")
+}
diff --git a/cmd/skupper/skupper_kube_expose.go b/cmd/skupper/skupper_kube_expose.go
index 031262e4..2bd7fe9b 100644
--- a/cmd/skupper/skupper_kube_expose.go
+++ b/cmd/skupper/skupper_kube_expose.go
@@ -84,6 +84,9 @@ func (s *SkupperKubeService) Expose(cmd *cobra.Command, args []string) error {
 	if exposeOpts.PublishNotReadyAddresses && targetType == "service" {
 		return fmt.Errorf("--publish-not-ready-addresses option is only valid for headless services and deployments")
 	}
+	if exposeOpts.Namespace != "" && targetType == "service" {
+		return targetTypeServiceTargetNamespaceError()
+	}
 
 	addr, err := expose(s.kube.Cli, context.Background(), targetType, targetName, exposeOpts)
 	if err == nil {
@@ -128,6 +131,10 @@ func (s *SkupperKubeService) Unexpose(cmd *cobra.Command, args []string) error {
 	silenceCobra(cmd)
 	targetType, targetName := parseTargetTypeAndName(args)
 
+	if unexposeNamespace != "" && targetType == "service" {
+		return targetTypeServiceTargetNamespaceError()
+	}
+
 	err := s.kube.Cli.ServiceInterfaceUnbind(context.Background(), targetType, targetName, unexposeAddress, true, unexposeNamespace)
 	if err == nil {
 		fmt.Printf("%s %s unexposed\n", targetType, targetName)
diff --git a/cmd/skupper/skupper_kube_service.go b/cmd/skupper/skupper_kube_service.go
index 1b8e97bc..2147b61a 100644
--- a/cmd/skupper/skupper_kube_service.go
+++ b/cmd/skupper/skupper_kube_service.go
@@ -154,6 +154,10 @@ func (s *SkupperKubeService) Bind(cmd *cobra.Command, args []string) error {
 		return fmt.Errorf("--publish-not-ready-addresses option is only valid for headless services and deployments")
 	}
 
+	if bindOptions.Namespace != "" && targetType == "service" {
+		return targetTypeServiceTargetNamespaceError()
+	}
+
 	service, err := s.kube.Cli.ServiceInterfaceInspect(context.Background(), args[0])
 
 	if err != nil {
@@ -194,6 +198,10 @@ func (s *SkupperKubeService) Unbind(cmd *cobra.Command, args []string) error {
 
 	targetType, targetName := parseTargetTypeAndName(args[1:])
 
+	if unbindNamespace != "" && targetType == "service" {
+		return targetTypeServiceTargetNamespaceError()
+	}
+
 	err := s.kube.Cli.ServiceInterfaceUnbind(context.Background(), targetType, targetName, args[0], false, unbindNamespace)
 	if err != nil {
 		return fmt.Errorf("%w", err)
diff --git a/cmd/skupper/skupper_kube_site.go b/cmd/skupper/skupper_kube_site.go
index bc186056..35aa343d 100644
--- a/cmd/skupper/skupper_kube_site.go
+++ b/cmd/skupper/skupper_kube_site.go
@@ -48,9 +48,12 @@ func (s *SkupperKubeSite) Create(cmd *cobra.Command, args []string) error {
 	routerCreateOpts.Labels = asMap(initFlags.labels)
 	routerCreateOpts.IngressAnnotations = asMap(s.kubeInit.ingressAnnotations)
 	routerCreateOpts.Router.ServiceAnnotations = asMap(s.kubeInit.routerServiceAnnotations)
+	routerCreateOpts.Router.PodAnnotations = asMap(s.kubeInit.routerPodAnnotations)
 	routerCreateOpts.Router.MaxFrameSize = types.RouterMaxFrameSizeDefault
 	routerCreateOpts.Router.MaxSessionFrames = types.RouterMaxSessionFramesDefault
 	routerCreateOpts.Controller.ServiceAnnotations = asMap(s.kubeInit.controllerServiceAnnotations)
+	routerCreateOpts.Controller.PodAnnotations = asMap(s.kubeInit.controllerPodAnnotations)
+	routerCreateOpts.PrometheusServer.PodAnnotations = asMap(s.kubeInit.prometheusServerPodAnnotations)
 	if err := routerCreateOpts.CheckIngress(); err != nil {
 		return err
 	}
@@ -109,7 +112,10 @@ func (s *SkupperKubeSite) CreateFlags(cmd *cobra.Command) {
 	s.kubeInit.ingressAnnotations = []string{}
 	s.kubeInit.annotations = []string{}
 	s.kubeInit.routerServiceAnnotations = []string{}
+	s.kubeInit.routerPodAnnotations = []string{}
 	s.kubeInit.controllerServiceAnnotations = []string{}
+	s.kubeInit.controllerPodAnnotations = []string{}
+	s.kubeInit.prometheusServerPodAnnotations = []string{}
 	cmd.Flags().BoolVarP(&routerCreateOpts.EnableConsole, "enable-console", "", false, "Enable skupper console must be used in conjunction with '--enable-flow-collector' flag")
 	cmd.Flag("ingress").Usage += " If not specified route is used when available, otherwise loadbalancer is used."
 	cmd.Flags().StringVarP(&routerCreateOpts.IngressHost, "ingress-host", "", "", "Hostname or alias by which the ingress route or proxy can be reached")
@@ -122,7 +128,10 @@ func (s *SkupperKubeSite) CreateFlags(cmd *cobra.Command) {
 	cmd.Flags().StringSliceVar(&s.kubeInit.ingressAnnotations, "ingress-annotations", []string{}, "Annotations to add to skupper ingress")
 	cmd.Flags().StringSliceVar(&s.kubeInit.annotations, "annotations", []string{}, "Annotations to add to skupper pods")
 	cmd.Flags().StringSliceVar(&s.kubeInit.routerServiceAnnotations, "router-service-annotations", []string{}, "Annotations to add to skupper router service")
+	cmd.Flags().StringSliceVar(&s.kubeInit.routerPodAnnotations, "router-pod-annotations", []string{}, "Annotations to add to skupper router pod")
 	cmd.Flags().StringSliceVar(&s.kubeInit.controllerServiceAnnotations, "controller-service-annotation", []string{}, "Annotations to add to skupper controller service")
+	cmd.Flags().StringSliceVar(&s.kubeInit.controllerPodAnnotations, "controller-pod-annotation", []string{}, "Annotations to add to skupper controller pod")
+	cmd.Flags().StringSliceVar(&s.kubeInit.prometheusServerPodAnnotations, "prometheus-server-pod-annotation", []string{}, "Annotations to add to skupper prometheus pod")
 	cmd.Flags().BoolVarP(&routerCreateOpts.EnableServiceSync, "enable-service-sync", "", true, "Participate in cross-site service synchronization")
 	cmd.Flags().DurationVar(&routerCreateOpts.SiteTtl, "service-sync-site-ttl", 0, "Time after which stale services, i.e. those whose site has not been heard from, created through service-sync are removed.")
 	cmd.Flags().BoolVarP(&routerCreateOpts.EnableFlowCollector, "enable-flow-collector", "", false, "Enable cross-site flow collection for the application network")
diff --git a/pkg/certs/certs.go b/pkg/certs/certs.go
index 294ab550..2266b55f 100644
--- a/pkg/certs/certs.go
+++ b/pkg/certs/certs.go
@@ -86,14 +86,17 @@ func getCAFromSecret(secret *corev1.Secret) CertificateAuthority {
 	}
 }
 
-func generateSecret(name string, subject string, hosts string, ca *CertificateAuthority) corev1.Secret {
+func generateSecret(name string, subject string, hosts string, ca *CertificateAuthority, expiration time.Duration) corev1.Secret {
 	priv, err := rsa.GenerateKey(rand.Reader, 2048)
 	if err != nil {
 		log.Fatalf("failed to generate private key: %s", err)
 	}
 
 	notBefore := time.Now()
-	notAfter := notBefore.Add(5 * 365 * 24 * time.Hour) // TODO: make configurable?
+	if expiration == 0 {
+		expiration = 5 * 365 * 24 * time.Hour
+	}
+	notAfter := notBefore.Add(expiration)
 
 	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
 	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
@@ -211,64 +214,16 @@ func CertDataToSecret(name string, certData CertificateData, annotations map[str
 }
 
 func GenerateSecret(name string, subject string, hosts string, ca *corev1.Secret) corev1.Secret {
-	caCert := getCAFromSecret(ca)
-	return generateSecret(name, subject, hosts, &caCert)
-}
-
-func GenerateCASecret(name string, subject string) corev1.Secret {
-	return generateSecret(name, subject, "", nil)
+	return GenerateSecretWithExpiration(name, subject, hosts, 0, ca)
 }
 
-func GenerateCertificateData(name string, subject string, hosts string, caData CertificateData) CertificateData {
-	caSecret := CertDataToSecret("temp", caData, nil)
-	secret := GenerateSecret(name, subject, hosts, &caSecret)
-	return SecretToCertData(secret)
-}
-
-func GenerateCACertificateData(name string, subject string) CertificateData {
-	secret := GenerateCASecret(name, subject)
-	return SecretToCertData(secret)
-}
-
-func PutCertificateData(name string, secretFile string, certData CertificateData, annotations map[string]string) {
-	secret := CertDataToSecret(name, certData, annotations)
-
-	// generate a yaml and save it to the specified path
-	s := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme.Scheme, scheme.Scheme)
-	out, err := os.Create(secretFile)
-	if err != nil {
-		log.Fatal("Could not write to file " + secretFile + ": " + err.Error())
-	}
-	err = s.Encode(&secret, out)
-	if err != nil {
-		log.Fatal("Could not write out generated secret: " + err.Error())
-	} else {
-		// TODO: valid token, local cluster? extra
-		fmt.Printf("Connection token written to %s", secretFile)
-		fmt.Println()
-	}
+func GenerateSecretWithExpiration(name string, subject string, hosts string, expiration time.Duration, ca *corev1.Secret) corev1.Secret {
+	caCert := getCAFromSecret(ca)
+	return generateSecret(name, subject, hosts, &caCert, expiration)
 }
 
-func GetSecretContent(secretFile string) (map[string][]byte, error) {
-	yaml, err := ioutil.ReadFile(secretFile)
-	if err != nil {
-		return nil, fmt.Errorf("Could not read connection token: %w", err)
-	}
-	s := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme.Scheme,
-		scheme.Scheme)
-	var secret corev1.Secret
-	_, _, err = s.Decode(yaml, nil, &secret)
-	if err != nil {
-		return nil, fmt.Errorf("Could not parse connection token: %w", err)
-	}
-	content := make(map[string][]byte)
-	for k, v := range secret.Data {
-		content[k] = v
-	}
-	for k, v := range secret.ObjectMeta.Annotations {
-		content[k] = []byte(v)
-	}
-	return content, nil
+func GenerateCASecret(name string, subject string) corev1.Secret {
+	return generateSecret(name, subject, "", nil, 0)
 }
 
 func GenerateSimpleSecret(name string, ca *corev1.Secret) corev1.Secret {
@@ -304,6 +259,14 @@ func GenerateSecretFile(secretFile string, secret *corev1.Secret, localOnly bool
 	return nil
 }
 
+func DecodeCertificate(data []byte) (*x509.Certificate, error) {
+	b, _ := pem.Decode(data)
+	if b == nil {
+		return nil, fmt.Errorf("Could not decode PEM block from data")
+	}
+	return x509.ParseCertificate(b.Bytes)
+}
+
 func GetTlsCrtHostnames(tlscrtData []byte) (subject string, hostnames []string, err error) {
 	b, _ := pem.Decode(tlscrtData)
 	if b == nil {
@@ -320,7 +283,31 @@ func GetTlsCrtHostnames(tlscrtData []byte) (subject string, hostnames []string,
 	return subject, hostnames, nil
 }
 
-func GetTlsConfig(verify bool, cert, key, ca string) (*tls.Config, error) {
+type TlsConfigRetriever struct {
+	Cert   string
+	Key    string
+	Ca     string
+	Verify bool
+}
+
+func (paths *TlsConfigRetriever) GetTlsConfig() (*tls.Config, error) {
+	tlsConfig, err := getTlsConfig(paths.Verify, paths.Cert, paths.Key, paths.Ca)
+	if err != nil {
+		return nil, err
+	}
+	return tlsConfig, nil
+}
+
+func GetTlsConfigRetriever(verify bool, cert, key, ca string) *TlsConfigRetriever {
+	return &TlsConfigRetriever{
+		Cert:   cert,
+		Key:    key,
+		Ca:     ca,
+		Verify: verify,
+	}
+}
+
+func getTlsConfig(verify bool, cert, key, ca string) (*tls.Config, error) {
 	var config tls.Config
 	config.InsecureSkipVerify = true
 	if verify {
diff --git a/pkg/domain/podman/controller/controller.go b/pkg/domain/podman/controller/controller.go
index c59a2ba0..00c61dcb 100644
--- a/pkg/domain/podman/controller/controller.go
+++ b/pkg/domain/podman/controller/controller.go
@@ -1,13 +1,13 @@
 package controller
 
 import (
-	"crypto/tls"
 	"fmt"
 	"log"
 	"time"
 
 	"github.com/skupperproject/skupper/api/types"
 	clientpodman "github.com/skupperproject/skupper/client/podman"
+	"github.com/skupperproject/skupper/pkg/certs"
 	"github.com/skupperproject/skupper/pkg/domain/podman"
 	"github.com/skupperproject/skupper/pkg/flow"
 	"github.com/skupperproject/skupper/pkg/qdr"
@@ -19,14 +19,14 @@ import (
 type ControllerPodman struct {
 	cli               *clientpodman.PodmanRestClient
 	cfg               *podman.Config
-	tlsConfig         *tls.Config
+	tlsConfig         *certs.TlsConfigRetriever
 	origin            string
 	containerInformer *clientpodman.ContainerInformer
 	servicesWatcher   *ServiceTargetWatcher
 	site              *podman.Site
 }
 
-func NewControllerPodman(origin string, tlsConfig *tls.Config) (*ControllerPodman, error) {
+func NewControllerPodman(origin string, tlsConfig *certs.TlsConfigRetriever) (*ControllerPodman, error) {
 	cfg, err := podman.NewPodmanConfigFileHandler().GetConfig()
 	if err != nil {
 		return nil, fmt.Errorf("error reading podman site config - %s", err)
diff --git a/pkg/domain/podman/router.go b/pkg/domain/podman/router.go
index 89e58033..62aa0158 100644
--- a/pkg/domain/podman/router.go
+++ b/pkg/domain/podman/router.go
@@ -72,7 +72,12 @@ func (r *RouterEntityManager) QueryConnections(routerId string, edge bool) ([]qd
 	var connections []qdr.Connection
 	err = json.Unmarshal([]byte(data), &connections)
 	if err != nil {
-		return nil, fmt.Errorf("error retrieving connections - %w", err)
+		if r.isInvalidResponseFromStaleRouter(data) {
+			fmt.Printf("Warning: unable to retrieve connections from router %q, as it is no longer available", routerId)
+			fmt.Println()
+		} else {
+			return nil, fmt.Errorf("error retrieving connections - %w - output: %q", err, data)
+		}
 	}
 	return connections, nil
 }
@@ -106,7 +111,10 @@ func (r *RouterEntityManager) QueryAllRouters() ([]qdr.Router, error) {
 		var records []qdr.Record
 		err = json.Unmarshal([]byte(rJson), &records)
 		if err != nil {
-			return nil, fmt.Errorf("error decoding router info from %s - %w", routerToQuery, err)
+			if r.isInvalidResponseFromStaleRouter(rJson) {
+				continue
+			}
+			return nil, fmt.Errorf("error decoding router info from %s - %w - %s", routerToQuery, err, rJson)
 		}
 		router.Site = qdr.GetSiteMetadata(records[0].AsString("metadata"))
 
@@ -145,6 +153,14 @@ func (r *RouterEntityManager) QueryAllRouters() ([]qdr.Router, error) {
 	return routersTmp, nil
 }
 
+// isInvalidResponseFromStaleRouter returns true if response is not a valid JSON
+// message returned by the management API, because the given router is stale as it
+// is still showing up as an existing router.node entity, but the connection is
+// no longer active.
+func (r *RouterEntityManager) isInvalidResponseFromStaleRouter(jsonOutput string) bool {
+	return strings.Contains(jsonOutput, "SendException: RELEASED") || strings.Contains(jsonOutput, "Timeout: ")
+}
+
 func (r *RouterEntityManager) QueryRouterNodes() ([]qdr.RouterNode, error) {
 	var routerNodes []qdr.RouterNode
 	// Retrieving all connections
diff --git a/pkg/flow/flow_mem_driver.go b/pkg/flow/flow_mem_driver.go
index 7709bf35..0202b655 100644
--- a/pkg/flow/flow_mem_driver.go
+++ b/pkg/flow/flow_mem_driver.go
@@ -300,7 +300,7 @@ func (fc *FlowCollector) annotateFlowTrace(flow *FlowRecord) *string {
 }
 
 func (fc *FlowCollector) linkFlowPair(flow *FlowRecord) (*FlowPairRecord, bool) {
-	var sourceSiteId, destSiteId string = "", ""
+	var sourceSiteId, destSiteId, sourceSiteName, destSiteName string = "", "", "sourceSite", "destSite"
 	var sourceFlow, destFlow *FlowRecord = nil, nil
 	var ok bool
 
@@ -330,14 +330,20 @@ func (fc *FlowCollector) linkFlowPair(flow *FlowRecord) (*FlowPairRecord, bool)
 	}
 
 	sourceSiteId = fc.getRecordSiteId(*sourceFlow)
+	if sourceSite, ok := fc.Sites[sourceSiteId]; ok {
+		sourceSiteName = *sourceSite.Name
+	}
 	destSiteId = fc.getRecordSiteId(*destFlow)
-	fwdLabels["sourceSite"] = sourceSiteId
-	fwdLabels["destSite"] = destSiteId
+	if destSite, ok := fc.Sites[destSiteId]; ok {
+		destSiteName = *destSite.Name
+	}
+	fwdLabels["sourceSite"] = sourceSiteName + "@_@" + sourceSiteId
+	fwdLabels["destSite"] = destSiteName + "@_@" + destSiteId
 	fwdLabels["sourceProcess"] = *sourceFlow.ProcessName
 	fwdLabels["destProcess"] = *destFlow.ProcessName
 	delete(fwdLabels, "process")
-	revLabels["sourceSite"] = destSiteId
-	revLabels["destSite"] = sourceSiteId
+	revLabels["sourceSite"] = destSiteName + "@_@" + destSiteId
+	revLabels["destSite"] = sourceSiteName + "@_@" + sourceSiteId
 	revLabels["sourceProcess"] = *destFlow.ProcessName
 	revLabels["destProcess"] = *sourceFlow.ProcessName
 	delete(revLabels, "process")
@@ -354,6 +360,9 @@ func (fc *FlowCollector) linkFlowPair(flow *FlowRecord) (*FlowPairRecord, bool)
 		ForwardFlow:       sourceFlow,
 		CounterFlow:       destFlow,
 	}
+	if sourceFlow.EndTime != 0 {
+		fp.Duration = sourceFlow.EndTime - sourceFlow.StartTime
+	}
 	if sourceSite, ok := fc.Sites[sourceSiteId]; ok {
 		fp.SourceSiteName = sourceSite.Name
 	} else {
@@ -453,7 +462,6 @@ func (fc *FlowCollector) addRecord(record interface{}) error {
 }
 
 func (fc *FlowCollector) deleteRecord(record interface{}) error {
-	// TODO: log the record
 	if record == nil {
 		return fmt.Errorf("No record to delete")
 	}
@@ -901,6 +909,7 @@ func (fc *FlowCollector) updateRecord(record interface{}) error {
 					if fc.getFlowPlace(current) == clientSide {
 						if flowpair, ok := fc.FlowPairs["fp-"+current.Identity]; ok {
 							flowpair.EndTime = current.EndTime
+							flowpair.Duration = flowpair.EndTime - flowpair.StartTime
 						}
 					}
 				}
@@ -1345,10 +1354,10 @@ func (fc *FlowCollector) retrieve(request ApiRequest) (*string, error) {
 			retrieveError = sortAndSlice(addresses, &p, queryParams)
 		case "item":
 			if id, ok := vars["id"]; ok {
-				if addr, ok := fc.VanAddresses[id]; ok {
-					fc.getAddressMetrics(addr)
+				if address, ok := fc.VanAddresses[id]; ok {
+					fc.getAddressMetrics(address)
 					p.Count = 1
-					p.Results = addr
+					p.Results = address
 				}
 			}
 		case "flows":
diff --git a/pkg/flow/record.go b/pkg/flow/record.go
index 0066497a..a1b26dbd 100644
--- a/pkg/flow/record.go
+++ b/pkg/flow/record.go
@@ -367,6 +367,7 @@ type FlowRecord struct {
 // Note a flowpair does not have a defined parent relationship through Base
 type FlowPairRecord struct {
 	Base
+	Duration                uint64      `json:"duration"`
 	Protocol                *string     `json:"protocol,omitempty"`
 	SourceSiteId            string      `json:"sourceSiteId,omitempty"`
 	SourceSiteName          *string     `json:"sourceSiteName,omitempty"`
@@ -661,7 +662,7 @@ func matchFieldValue(x interface{}, y string) bool {
 	if x != nil && y != "" {
 		switch x.(type) {
 		case string:
-			return x.(string) == y
+			return strings.HasPrefix(x.(string), y)
 		case uint64:
 			i, err := strconv.ParseInt(y, 10, 64)
 			if err == nil {
diff --git a/pkg/images/image_utils.go b/pkg/images/image_utils.go
index eadde1af..990f3aa9 100644
--- a/pkg/images/image_utils.go
+++ b/pkg/images/image_utils.go
@@ -17,14 +17,17 @@ const (
 	ConfigSyncImageEnvKey             string = "SKUPPER_CONFIG_SYNC_IMAGE"
 	FlowCollectorImageEnvKey          string = "SKUPPER_FLOW_COLLECTOR_IMAGE"
 	PrometheusServerImageEnvKey       string = "PROMETHEUS_SERVER_IMAGE"
+	OauthProxyImageEnvKey             string = "OAUTH_PROXY_IMAGE"
 	RouterPullPolicyEnvKey            string = "QDROUTERD_IMAGE_PULL_POLICY"
 	ServiceControllerPullPolicyEnvKey string = "SKUPPER_SERVICE_CONTROLLER_IMAGE_PULL_POLICY"
 	ControllerPodmanPullPolicyEnvKey  string = "SKUPPER_CONTROLLER_PODMAN_IMAGE_PULL_POLICY"
 	ConfigSyncPullPolicyEnvKey        string = "SKUPPER_CONFIG_SYNC_IMAGE_PULL_POLICY"
 	FlowCollectorPullPolicyEnvKey     string = "SKUPPER_FLOW_COLLECTOR_IMAGE_PULL_POLICY"
 	PrometheusServerPullPolicyEnvKey  string = "PROMETHEUS_SERVER_IMAGE_PULL_POLICY"
+	OauthProxyPullPolicyEnvKey        string = "OAUTH_PROXY_IMAGE_PULL_POLICY"
 	SkupperImageRegistryEnvKey        string = "SKUPPER_IMAGE_REGISTRY"
 	PrometheusImageRegistryEnvKey     string = "PROMETHEUS_IMAGE_REGISTRY"
+	OauthProxyRegistryEnvKey          string = "OAUTH_PROXY_IMAGE_REGISTRY"
 )
 
 func getPullPolicy(key string) string {
@@ -220,3 +223,32 @@ func GetSha(imageName string) string {
 
 	return digest
 }
+
+func GetOauthProxyImageName() string {
+	image := os.Getenv(OauthProxyImageEnvKey)
+	if image == "" {
+		imageRegistry := GetOauthProxyImageRegistry()
+		return strings.Join([]string{imageRegistry, OauthProxyImageName}, "/")
+	} else {
+		return image
+	}
+}
+
+func GetOauthProxyImagePullPolicy() string {
+	return getPullPolicy(OauthProxyPullPolicyEnvKey)
+}
+
+func GetOauthProxyImageDetails() types.ImageDetails {
+	return types.ImageDetails{
+		Name:       GetOauthProxyImageName(),
+		PullPolicy: GetOauthProxyImagePullPolicy(),
+	}
+}
+
+func GetOauthProxyImageRegistry() string {
+	imageRegistry := os.Getenv(OauthProxyRegistryEnvKey)
+	if imageRegistry == "" {
+		return OauthProxyImageRegistry
+	}
+	return imageRegistry
+}
diff --git a/pkg/images/images.go b/pkg/images/images.go
index 1c2c8555..f6a13424 100644
--- a/pkg/images/images.go
+++ b/pkg/images/images.go
@@ -10,4 +10,6 @@ const (
 	SiteControllerImageName    string = "site-controller:main"
 	PrometheusImageRegistry    string = "quay.io/prometheus"
 	PrometheusServerImageName  string = "prometheus:v2.42.0"
+	OauthProxyImageRegistry    string = "quay.io/openshift"
+	OauthProxyImageName        string = "origin-oauth-proxy:4.14.0"
 )
diff --git a/pkg/kube/misc.go b/pkg/kube/misc.go
index c08b7bc5..72111f92 100644
--- a/pkg/kube/misc.go
+++ b/pkg/kube/misc.go
@@ -17,12 +17,13 @@ package kube
 import (
 	"context"
 	"fmt"
-	"github.com/openshift/client-go/apps/clientset/versioned"
-	"k8s.io/client-go/kubernetes"
 	"reflect"
 	"strconv"
 	"strings"
 
+	"github.com/openshift/client-go/apps/clientset/versioned"
+	"k8s.io/client-go/kubernetes"
+
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/pkg/utils"
 	corev1 "k8s.io/api/core/v1"
@@ -69,14 +70,13 @@ func GetServiceInterfaceTarget(targetType string, targetName string, deducePort
 	} else if targetType == "pods" {
 		return nil, fmt.Errorf("VAN service interfaces for pods not yet implemented")
 	} else if targetType == "service" {
-		var svcName, svcNamespace string = GetServiceName(targetName, namespace)
-		var formattedName = fmt.Sprintf("%s.%s", svcName, svcNamespace)
 		target := types.ServiceInterfaceTarget{
-			Name:      formattedName,
-			Service:   formattedName,
-			Namespace: namespace,
+			Name:    targetName,
+			Service: targetName,
+			// decided to keep namespace empty when target type is a service
+			Namespace: "",
 		}
-		if deducePort {
+		if deducePort && !strings.Contains(targetName, ".") {
 			ports, err := GetPortsForServiceTarget(targetName, namespace, cli)
 			if err != nil {
 				return nil, err
diff --git a/pkg/qdr/amqp_mgmt.go b/pkg/qdr/amqp_mgmt.go
index ad6bbdc5..71cb8ab4 100644
--- a/pkg/qdr/amqp_mgmt.go
+++ b/pkg/qdr/amqp_mgmt.go
@@ -2,7 +2,6 @@ package qdr
 
 import (
 	"context"
-	"crypto/tls"
 	"encoding/json"
 	"fmt"
 	"log"
@@ -190,11 +189,11 @@ func (node *RouterNode) AsRouter() *Router {
 
 type AgentPool struct {
 	url    string
-	config *tls.Config
+	config TlsConfigRetriever
 	pool   chan *Agent
 }
 
-func NewAgentPool(url string, config *tls.Config) *AgentPool {
+func NewAgentPool(url string, config TlsConfigRetriever) *AgentPool {
 	return &AgentPool{
 		url:    url,
 		config: config,
@@ -223,7 +222,7 @@ func (p *AgentPool) Put(a *Agent) {
 	}
 }
 
-func Connect(url string, config *tls.Config) (*Agent, error) {
+func Connect(url string, config TlsConfigRetriever) (*Agent, error) {
 	factory := ConnectionFactory{
 		url:    url,
 		config: config,
diff --git a/pkg/qdr/messaging.go b/pkg/qdr/messaging.go
index 137469da..df52e69a 100644
--- a/pkg/qdr/messaging.go
+++ b/pkg/qdr/messaging.go
@@ -9,16 +9,24 @@ import (
 	"github.com/skupperproject/skupper/pkg/messaging"
 )
 
+type TlsConfigRetriever interface {
+	GetTlsConfig() (*tls.Config, error)
+}
+
 type ConnectionFactory struct {
 	url    string
-	config *tls.Config
+	config TlsConfigRetriever
 }
 
 func (f *ConnectionFactory) Connect() (messaging.Connection, error) {
 	if f.config == nil {
 		return dial(f.url, amqp.ConnMaxFrameSize(4294967295))
 	} else {
-		return dial(f.url, amqp.ConnSASLExternal(), amqp.ConnMaxFrameSize(4294967295), amqp.ConnTLSConfig(f.config))
+		tlsConfig, err := f.config.GetTlsConfig()
+		if err != nil {
+			return nil, err
+		}
+		return dial(f.url, amqp.ConnSASLExternal(), amqp.ConnMaxFrameSize(4294967295), amqp.ConnTLSConfig(tlsConfig))
 	}
 }
 
@@ -39,7 +47,7 @@ func (f *ConnectionFactory) Url() string {
 	return f.url
 }
 
-func NewConnectionFactory(url string, config *tls.Config) *ConnectionFactory {
+func NewConnectionFactory(url string, config TlsConfigRetriever) *ConnectionFactory {
 	return &ConnectionFactory{
 		url:    url,
 		config: config,
diff --git a/pkg/site/site.go b/pkg/site/site.go
index ef28e2f2..8e99bcb3 100644
--- a/pkg/site/site.go
+++ b/pkg/site/site.go
@@ -53,6 +53,7 @@ const (
 	SiteConfigPrometheusServerMemoryKey         string = "prometheus-server-memory"
 	SiteConfigPrometheusServerCpuLimitKey       string = "prometheus-server-cpu-limit"
 	SiteConfigPrometheusServerMemoryLimitKey    string = "prometheus-server-memory-limit"
+	SiteConfigPrometheusServerPodAnnotationsKey string = "prometheus-server-pod-annotations"
 
 	// router options
 	SiteConfigRouterConsoleKey             string = "router-console"
@@ -70,6 +71,7 @@ const (
 	SiteConfigRouterDataConnectionCountKey string = "router-data-connection-count"
 	SiteConfigRouterIngressHostKey         string = "router-ingress-host"
 	SiteConfigRouterServiceAnnotationsKey  string = "router-service-annotations"
+	SiteConfigRouterPodAnnotationsKey      string = "router-pod-annotations"
 	SiteConfigRouterLoadBalancerIp         string = "router-load-balancer-ip"
 	SiteConfigRouterDisableMutualTLS       string = "router-disable-mutual-tls"
 
@@ -86,6 +88,7 @@ const (
 	SiteConfigControllerNodeSelectorKey       string = "controller-node-selector"
 	SiteConfigControllerIngressHostKey        string = "controller-ingress-host"
 	SiteConfigControllerServiceAnnotationsKey string = "controller-service-annotations"
+	SiteConfigControllerPodAnnotationsKey     string = "controller-pod-annotations"
 	SiteConfigControllerLoadBalancerIp        string = "controller-load-balancer-ip"
 
 	// config-sync options
@@ -257,6 +260,13 @@ func WriteSiteConfig(spec types.SiteConfigSpec, namespace string) (*corev1.Confi
 		}
 		siteConfig.Data[SiteConfigRouterServiceAnnotationsKey] = strings.Join(annotations, ",")
 	}
+	if len(spec.Router.PodAnnotations) > 0 {
+		var annotations []string
+		for key, value := range spec.Router.PodAnnotations {
+			annotations = append(annotations, key+"="+value)
+		}
+		siteConfig.Data[SiteConfigRouterPodAnnotationsKey] = strings.Join(annotations, ",")
+	}
 	if spec.Router.LoadBalancerIp != "" {
 		siteConfig.Data[SiteConfigRouterLoadBalancerIp] = spec.Router.LoadBalancerIp
 	}
@@ -310,6 +320,13 @@ func WriteSiteConfig(spec types.SiteConfigSpec, namespace string) (*corev1.Confi
 		}
 		siteConfig.Data[SiteConfigControllerServiceAnnotationsKey] = strings.Join(annotations, ",")
 	}
+	if len(spec.Controller.PodAnnotations) > 0 {
+		var annotations []string
+		for key, value := range spec.Controller.PodAnnotations {
+			annotations = append(annotations, key+"="+value)
+		}
+		siteConfig.Data[SiteConfigControllerPodAnnotationsKey] = strings.Join(annotations, ",")
+	}
 	if spec.Controller.LoadBalancerIp != "" {
 		siteConfig.Data[SiteConfigControllerLoadBalancerIp] = spec.Controller.LoadBalancerIp
 	}
@@ -421,6 +438,13 @@ func WriteSiteConfig(spec types.SiteConfigSpec, namespace string) (*corev1.Confi
 			siteConfig.Data[SiteConfigPrometheusServerMemoryLimitKey] = spec.PrometheusServer.MemoryLimit
 		}
 	}
+	if len(spec.PrometheusServer.PodAnnotations) > 0 {
+		var annotations []string
+		for key, value := range spec.PrometheusServer.PodAnnotations {
+			annotations = append(annotations, key+"="+value)
+		}
+		siteConfig.Data[SiteConfigPrometheusServerPodAnnotationsKey] = strings.Join(annotations, ",")
+	}
 
 	// TODO: allow Replicas to be set through skupper-site configmap?
 	if !spec.SiteControlled {
@@ -646,6 +670,9 @@ func ReadSiteConfig(siteConfig *corev1.ConfigMap, namespace string, defaultIngre
 	if routerServiceAnnotations, ok := siteConfig.Data[SiteConfigRouterServiceAnnotationsKey]; ok {
 		result.Spec.Router.ServiceAnnotations = asMap(splitWithEscaping(routerServiceAnnotations, ',', '\\'))
 	}
+	if routerPodAnnotations, ok := siteConfig.Data[SiteConfigRouterPodAnnotationsKey]; ok {
+		result.Spec.Router.PodAnnotations = asMap(splitWithEscaping(routerPodAnnotations, ',', '\\'))
+	}
 	if routerServiceLoadBalancerIp, ok := siteConfig.Data[SiteConfigRouterLoadBalancerIp]; ok {
 		result.Spec.Router.LoadBalancerIp = routerServiceLoadBalancerIp
 	}
@@ -680,6 +707,9 @@ func ReadSiteConfig(siteConfig *corev1.ConfigMap, namespace string, defaultIngre
 	if controllerServiceAnnotations, ok := siteConfig.Data[SiteConfigControllerServiceAnnotationsKey]; ok {
 		result.Spec.Controller.ServiceAnnotations = asMap(splitWithEscaping(controllerServiceAnnotations, ',', '\\'))
 	}
+	if controllerPodAnnotations, ok := siteConfig.Data[SiteConfigControllerPodAnnotationsKey]; ok {
+		result.Spec.Controller.PodAnnotations = asMap(splitWithEscaping(controllerPodAnnotations, ',', '\\'))
+	}
 	if controllerServiceLoadBalancerIp, ok := siteConfig.Data[SiteConfigControllerLoadBalancerIp]; ok {
 		result.Spec.Controller.LoadBalancerIp = controllerServiceLoadBalancerIp
 	}
@@ -746,6 +776,10 @@ func ReadSiteConfig(siteConfig *corev1.ConfigMap, namespace string, defaultIngre
 	if prometheusMemoryLimit, ok := siteConfig.Data[SiteConfigPrometheusServerMemoryLimitKey]; ok && prometheusMemoryLimit != "" {
 		result.Spec.PrometheusServer.MemoryLimit = prometheusMemoryLimit
 	}
+	if prometheusPodAnnotations, ok := siteConfig.Data[SiteConfigPrometheusServerPodAnnotationsKey]; ok {
+		result.Spec.PrometheusServer.PodAnnotations = asMap(splitWithEscaping(prometheusPodAnnotations, ',', '\\'))
+	}
+
 	annotationExclusions := []string{}
 	labelExclusions := []string{}
 	annotations := map[string]string{}


Test patch:
diff --git a/client/serviceinterface_update_test.go b/client/serviceinterface_update_test.go
index 7b9edbd5..4672b066 100644
--- a/client/serviceinterface_update_test.go
+++ b/client/serviceinterface_update_test.go
@@ -130,7 +130,7 @@ var httpDeployment *appsv1.Deployment = &appsv1.Deployment{
 				Containers: []corev1.Container{
 					{
 						Name:            "nginx",
-						Image:           "nginxinc/nginx-unprivileged",
+						Image:           "quay.io/skupper/nginx-unprivileged",
 						ImagePullPolicy: corev1.PullIfNotPresent,
 						Ports: []corev1.ContainerPort{
 							{
diff --git a/pkg/domain/podman/main_test.go b/pkg/domain/podman/main_test.go
index 9128927c..78abfd00 100644
--- a/pkg/domain/podman/main_test.go
+++ b/pkg/domain/podman/main_test.go
@@ -32,7 +32,7 @@ var (
 const (
 	ENV_PODMAN_ENDPOINT = "SKUPPER_TEST_PODMAN_ENDPOINT"
 	NS                  = "podman-system-test"
-	NGINX_IMAGE         = "docker.io/nginxinc/nginx-unprivileged:stable-alpine"
+	NGINX_IMAGE         = "quay.io/skupper/nginx-unprivileged:stable-alpine"
 )
 
 // newBasicSite returns a new instance of a basic Site instance
diff --git a/pkg/flow/record_test.go b/pkg/flow/record_test.go
index 8f9a4107..9564f970 100644
--- a/pkg/flow/record_test.go
+++ b/pkg/flow/record_test.go
@@ -676,6 +676,18 @@ func TestFilterFieldsRecord(t *testing.T) {
 			filterField: map[string]string{"sourceSiteName": "public1"},
 			result:      true,
 		},
+		{
+			filterField: map[string]string{"sourceSiteName": "pub"},
+			result:      true,
+		},
+		{
+			filterField: map[string]string{"sourceSiteName": "ic1"},
+			result:      false,
+		},
+		{
+			filterField: map[string]string{"sourceSiteName": "xpublic1"},
+			result:      false,
+		},
 		{
 			filterField: map[string]string{"forwardFlow.SourceHost": "10.20.30.40", "sourceSiteName": "public1"},
 			result:      true,
diff --git a/pkg/qdr/amqp_mgmt_mock_test.go b/pkg/qdr/amqp_mgmt_mock_test.go
index 0081766d..bc186660 100644
--- a/pkg/qdr/amqp_mgmt_mock_test.go
+++ b/pkg/qdr/amqp_mgmt_mock_test.go
@@ -5,7 +5,6 @@ import (
 	"reflect"
 	"testing"
 
-	"crypto/tls"
 	"encoding/json"
 
 	"gotest.tools/assert"
@@ -103,7 +102,7 @@ func TestQDR(t *testing.T) {
 	assert.Assert(t, router2.Id == router_node.Id && router2.Edge == false, "RouterNode.AsRouter failure: Id |%s|  Edge: %t.", router2.Id, router2.Edge)
 
 	// NewAgentPool ------------------------------------
-	agentPool := NewAgentPool("my_url", &tls.Config{})
+	agentPool := NewAgentPool("my_url", nil)
 	assert.Assert(t, agentPool.url == "my_url", "NewAgentPool failure")
 
 	// Get ------------------------------------
diff --git a/test/images/Dockerfile.nghttp2 b/test/images/Dockerfile.nghttp2
new file mode 100644
index 00000000..6b69a2ff
--- /dev/null
+++ b/test/images/Dockerfile.nghttp2
@@ -0,0 +1,9 @@
+FROM alpine:latest
+
+LABEL description="A supporting test image containing nghttp2 (client, server and load tester), for Skupper testing.  By default, this image does not start any nghttp2 project; the user needs to overwrite CMD with the command they want"
+LABEL maintainer="Skupper project"
+LABEL origin=https://github.com/skupperproject/skupper/blob/main/test/images
+
+RUN apk add --no-cache nghttp2
+
+CMD ["ls"]
diff --git a/test/images/Makefile b/test/images/Makefile
new file mode 100644
index 00000000..acc5dfbb
--- /dev/null
+++ b/test/images/Makefile
@@ -0,0 +1,180 @@
+# This make file provides three operations:
+#
+# - Copy a list of images (hardcoded below) to a main repository (by default,
+#   quay.io/skupper)
+# - Generate and push images to that same repository, based on Dockerfiles
+# - Copy both lists of images above (external and generated) to another,
+#   configurable repository
+#
+# See the README.md for the rationale of each of them.
+#
+# The basic operation is to copy or generate-and-push a single image into
+# the MAIN_REPO:
+#
+#   make docker.io/svagi/nghttp2
+#   make nghttp2
+#
+# The possible values are those listed on EXTERNAL_IMAGES and GENERATED_IMAGES.
+#
+# You can also only build an image (and not push it).  Just add _build to the
+# image name listed in GENERATED_IMAGES:
+#
+#   make nghttp2_build
+#
+# Alternativelly, one can build/push or copy all generated, all external, or
+# all of them:
+#
+#   make external_images
+#   make generated_images
+#   make everything
+#
+# Notice that the Makefile itself will not check whether the copy or build/push
+# is necessary; the underlying tools (podman and skopeo) may do that, but they
+# may still consume Dockerhub pull requests in the process.
+#
+# As with any Makefile, you can configure the execution by setting variables
+# on the make invokation:
+#
+#   make MAIN_REPO=quay.io/dhashimo docker.io/ssorj/quiver
+#
+# The command above will copy docker.io/ssorj/quiver into the quay repository
+# quay.io/dhashimo, instead of the default MAIN_REPO
+#
+#   make MAIN_REPO=quay.io/dhashimo everything
+#
+# Similarly, the command above will copy and build/generate everything, but
+# into the specified MAIN_REPO.
+#
+# The last operation is to copy images from MAIN_REPO to another repository,
+# which needs to be specified.
+#
+#   make COPY_REPO=192.168.0.1:5000/internal copy
+#
+# The command above will copy all images (external and generated, as listed
+# below) from the MAIN_REPO into the 'internal' repository within the
+# registry running at 192.168.0.1:5000.
+#
+# Individual items can also be specified.  For that, just add '_copy' to
+# their main target names:
+#
+#   make COPY_REPO=192.168.0.1:5000/internal nghttp2_copy
+#   make COPY_REPO=192.168.0.1:5000/internal docker.io/ssorj/quiver_copy
+#
+# This Makefile is intended to be executed manually, with the login of
+# individual users, and not by a robot account on a CI.
+#
+# ATTENTION: Remember that images copied for the first time into quay may
+#            be created as 'private' images, and need to be changed into
+#            'public' before they can be used by CI and other users.
+
+# --- configuration ---
+
+# tools
+SHELL = /bin/sh
+PODMAN = podman
+SKOPEO = skopeo
+
+# tool options
+
+# The options below create or transform the images into the docker format.
+# That is required for their use with Openshift 3.11
+FORMAT_OPTIONS = --format docker
+TRANSFORM_OPTIONS = --format v2s1
+
+# Repositories
+MAIN_REPO = quay.io/skupper
+COPY_REPO = localhost:5000/local
+
+# This is the list of external images that will be copied to $(MAIN_REPO).  If
+# the image specifies a tag, make sure to escape the colon with a backslash
+# (such as in docker.io/library/mongo\:5.0).  Otherwise, you'll get an error
+# like `multiple target patterns.  Stop.`
+EXTERNAL_IMAGES := \
+	docker.io/nginxinc/nginx-unprivileged \
+	docker.io/ssorj/quiver \
+	docker.io/library/postgres \
+	docker.io/library/redis \
+	docker.io/maistra/examples-bookinfo-productpage-v1\:0.12.0 \
+	docker.io/maistra/examples-bookinfo-ratings-v1\:0.12.0 \
+	docker.io/maistra/examples-bookinfo-details-v1\:0.12.0 \
+	docker.io/maistra/examples-bookinfo-reviews-v3\:0.12.0 \
+	docker.io/library/mongo\:5.0 \
+	docker.io/nginxinc/nginx-unprivileged\:stable-alpine
+
+# These are the images that are generated by this Makefile (as opposed to
+# created elsewhere and just copied here).  To add a new image, simply create
+# Dockerfile.IMAGENAME and add the IMAGENAME below.
+GENERATED_IMAGES := \
+	nghttp2
+
+# --- end of configuration ---
+
+
+# These are the build target names, such as nghttp2_build, generated
+# automatically from GENERATED_IMAGES.  Do not overwrite this
+generated_build := $(patsubst %,%_build,$(GENERATED_IMAGES))
+
+# The copy target names, to be used with the copy operation
+generated_copy := $(patsubst %,%_copy,$(GENERATED_IMAGES))
+external_copy   := $(patsubst %,%_copy,$(EXTERNAL_IMAGES))
+
+# We don't want someone to just run `make` and start copying stuff
+# around, so instead the default operation is to just give some
+# hint on how to use it.
+all:
+	@echo "Use 'make everything' for building and copying everything to the MAIN_REPO".
+	@echo
+	@echo "MAIN_REPO=$(MAIN_REPO)"
+	@echo
+	@echo Normal use, however, is to select individual targets.
+	@echo Check Makefile contents for documentation.
+
+# For a generated image, we simply have a Dockerfile named after it and call
+# `podman build` on it, tagging it on the MAIN_REPO
+$(generated_build): TARGET = $(patsubst %_build,%,$@)
+$(generated_build):
+	$(PODMAN) build $(FORMAT_OPTIONS) --file Dockerfile.$(TARGET) --tag $(MAIN_REPO)/$(TARGET)
+
+# This is generic the push target, for images built here (GENERATED_IMAGES)
+# Each image depends on its respective _build target
+%: %_build
+	$(PODMAN) push $(MAIN_REPO)/$@
+
+# Shortcut targets
+external_images: $(EXTERNAL_IMAGES)
+generated_images: $(GENERATED_IMAGES)
+everything: external_images generated_images
+
+# This is the main target for the external images; it copies them from their
+# original locations _into_ MAIN_REPO.  Do not confuse this for the `copy` operation
+# below, that copies _from_ MAIN_REPO into COPY_REPO.
+#
+# The first line prepares a target-local TARGET variable, which contains only
+# the last part of the URL
+$(EXTERNAL_IMAGES): TARGET = $(shell echo "$@" | sed s_.*/__ )
+$(EXTERNAL_IMAGES):
+	$(SKOPEO) copy $(TRANSFORM_OPTIONS) \
+		docker://$@ \
+		docker://$(MAIN_REPO)/$(TARGET)
+
+#
+# Copy operation
+#
+copy: $(generated_copy) $(external_copy)
+
+# The targets for external and generated images are basically the same; the
+# only difference is the way the original/main target needs to be manipulated
+# to generate the image name+tag (saved in the target-local variable TARGET).
+
+$(generated_copy): TARGET = $(patsubst %_copy,%,$@)
+$(generated_copy):
+	$(SKOPEO) copy $(TRANSFORM_OPTIONS) \
+		docker://$(MAIN_REPO)/$(TARGET) \
+		docker://$(COPY_REPO)/$(TARGET)
+
+# Remove _copy, but also everything till the last /
+$(external_copy): TARGET = $(shell echo "$@" | sed -e "s/_copy$$//" -e 's_.*/__' )
+$(external_copy):
+	$(SKOPEO) copy $(TRANSFORM_OPTIONS) \
+		docker://$(MAIN_REPO)/$(TARGET) \
+		docker://$(COPY_REPO)/$(TARGET)
diff --git a/test/images/README.md b/test/images/README.md
new file mode 100644
index 00000000..7e52f780
--- /dev/null
+++ b/test/images/README.md
@@ -0,0 +1,37 @@
+# Images for external dependencies
+
+The Makefile on this directory allows for the manipulation of container images
+pertaining to external test dependencies, such as Mongo or quiver.
+
+It contains three operations: the first exists to avoid the Dockerhub pull
+limit, and also to allow for testing on disconnected clusters.  It simply
+copies the images from their original locations into our Quay repo.
+
+The second operation was created in response to an image used by our tests that
+stopped getting updates, while the actual project continued to evolve.  It
+builds images from Dockerfile and pushes them into our Quay repo.
+
+Finally, the third one also allows for the running of tests on disconnected
+clusters, which cannot access the original locations of these dependencies to
+download them (as they have no external Internet access).  The operation allows
+for copying the list of images from our Quay repo into another repo.
+
+This allows for the test dependencies to be copied to the disconnected
+cluster's local repository, from where the cluster can access them.  Additional
+configuration will be required on the cluster (such as creating an MCO to
+re-route the image pull requests), but that's out of the scope of this
+document.
+
+Note that this is not a full repo copy; it's restricted to the list of images
+contained on the Makefile.  Skupper images (such as the router or controller)
+are not copied by this operation.
+
+See the Makefile contents for information on how to execute the different
+operations.
+
+
+# skupper-test image
+
+Note that the `skupper-test` image used by Skupper integration is part of
+Skupper's own test code, so it's build by the main Makefile at ../.., and not
+here.
diff --git a/test/integration/acceptance/annotation/annotated_resource.go b/test/integration/acceptance/annotation/annotated_resource.go
index 94fb7002..d01dd163 100644
--- a/test/integration/acceptance/annotation/annotated_resource.go
+++ b/test/integration/acceptance/annotation/annotated_resource.go
@@ -250,7 +250,7 @@ func createDeployment(cluster *client.VanClient, annotations map[string]string)
 				},
 				Spec: corev1.PodSpec{
 					Containers: []corev1.Container{
-						{Name: "nginx", Image: "nginxinc/nginx-unprivileged:stable-alpine", Ports: []corev1.ContainerPort{{Name: "web", ContainerPort: 8080}}, ImagePullPolicy: corev1.PullIfNotPresent},
+						{Name: "nginx", Image: "quay.io/skupper/nginx-unprivileged:stable-alpine", Ports: []corev1.ContainerPort{{Name: "web", ContainerPort: 8080}}, ImagePullPolicy: corev1.PullIfNotPresent},
 					},
 					RestartPolicy: corev1.RestartPolicyAlways,
 				},
@@ -325,7 +325,7 @@ func createStatefulSet(cluster *client.VanClient, annotations map[string]string)
 				},
 				Spec: corev1.PodSpec{
 					Containers: []corev1.Container{
-						{Name: "nginx", Image: "nginxinc/nginx-unprivileged:stable-alpine", Ports: []corev1.ContainerPort{{Name: "web", ContainerPort: 8080}}, ImagePullPolicy: corev1.PullIfNotPresent},
+						{Name: "nginx", Image: "quay.io/skupper/nginx-unprivileged:stable-alpine", Ports: []corev1.ContainerPort{{Name: "web", ContainerPort: 8080}}, ImagePullPolicy: corev1.PullIfNotPresent},
 					},
 					RestartPolicy: corev1.RestartPolicyAlways,
 				},
@@ -373,7 +373,7 @@ func createDaemonSet(cluster *client.VanClient, annotations map[string]string) (
 				},
 				Spec: corev1.PodSpec{
 					Containers: []corev1.Container{
-						{Name: "nginx", Image: "nginxinc/nginx-unprivileged:stable-alpine", Ports: []corev1.ContainerPort{{Name: "web", ContainerPort: 8080}}, ImagePullPolicy: corev1.PullIfNotPresent},
+						{Name: "nginx", Image: "quay.io/skupper/nginx-unprivileged:stable-alpine", Ports: []corev1.ContainerPort{{Name: "web", ContainerPort: 8080}}, ImagePullPolicy: corev1.PullIfNotPresent},
 					},
 					RestartPolicy: corev1.RestartPolicyAlways,
 				},
diff --git a/test/integration/acceptance/custom/headless/headless.go b/test/integration/acceptance/custom/headless/headless.go
index 1191b784..ccfaf745 100644
--- a/test/integration/acceptance/custom/headless/headless.go
+++ b/test/integration/acceptance/custom/headless/headless.go
@@ -26,7 +26,7 @@ const (
 	appPort     = "8080"
 	appPortInt  = 8080
 	appPortName = "web"
-	appImage    = "nginxinc/nginx-unprivileged:stable-alpine"
+	appImage    = "quay.io/skupper/nginx-unprivileged:stable-alpine"
 	pod0Name    = appName + "-0"
 	pod1Name    = appName + "-1"
 )
diff --git a/test/integration/examples/custom/helloworld/helloworld_podman_test.go b/test/integration/examples/custom/helloworld/helloworld_podman_test.go
index 5e20620b..d9cce381 100644
--- a/test/integration/examples/custom/helloworld/helloworld_podman_test.go
+++ b/test/integration/examples/custom/helloworld/helloworld_podman_test.go
@@ -58,7 +58,8 @@ func TestHelloWorldCLIOnPodman(t *testing.T) {
 	assert.Assert(t, err)
 
 	// Validating if podman site exists
-	prvSiteHandler, _ := podman.NewSitePodmanHandler("")
+	prvSiteHandler, err := podman.NewSitePodmanHandler("")
+	assert.Assert(t, err)
 	prvSite, _ := prvSiteHandler.Get()
 	assert.Assert(t, prvSite == nil, "podman site already exists")
 
@@ -157,7 +158,7 @@ func TestHelloWorldCLIOnPodman(t *testing.T) {
 						RouterMode:          "interior",
 						EnableConsole:       true,
 						EnableFlowCollector: true,
-						RunAsUser:           "1000",
+						RunAsUser:           getRunAsUserOrDefault("1000", pub),
 						RunAsGroup:          "2000",
 					},
 					// skupper status - verify initialized as interior
diff --git a/test/integration/examples/custom/helloworld/helloworld_test.go b/test/integration/examples/custom/helloworld/helloworld_test.go
index 15e858a3..db98a8d0 100644
--- a/test/integration/examples/custom/helloworld/helloworld_test.go
+++ b/test/integration/examples/custom/helloworld/helloworld_test.go
@@ -7,8 +7,6 @@ import (
 	"context"
 	"log"
 	"os"
-	"strconv"
-	"strings"
 	"testing"
 
 	"github.com/skupperproject/skupper/api/types"
@@ -82,29 +80,6 @@ func TestHelloWorldCLI(t *testing.T) {
 	// deploying frontend and backend services
 	assert.Assert(t, deployResources(pub, prv))
 
-	var runAsUser = "1000"
-
-	// OpenShift requires container user IDs to exist within a range; we try to satisfy it here.
-	namespace, err := pub.VanClient.KubeClient.CoreV1().Namespaces().Get(context.Background(), pub.Namespace, metav1.GetOptions{})
-	if err != nil {
-		log.Printf("Unable to get namespace %q; using pre-defined runAsUser value %v", pub.Namespace, runAsUser)
-		// We do not fail here; we just try the test with the pre-defined value
-	} else {
-		// On this block, we just ignore any errors and use the pre-existing value
-		ns_annotations := namespace.GetAnnotations()
-		if users, ok := ns_annotations["openshift.io/sa.scc.uid-range"]; ok {
-			log.Printf("OpenShift UID range annotation found: %q", users)
-			// format is like 1000860000/10000, where the first number is the
-			// range start, and the second its length
-			split_users := strings.Split(users, "/")
-			if split_users[0] != "" {
-				if _, err := strconv.Atoi(split_users[0]); err == nil {
-					runAsUser = split_users[0]
-				}
-			}
-		}
-	}
-
 	// These test scenarios allow defining a set of skupper cli
 	// commands to be executed as a workflow, against specific
 	// clusters. Each execution is validated accordingly by its
@@ -129,7 +104,7 @@ func TestHelloWorldCLI(t *testing.T) {
 						RouterMode:          "interior",
 						EnableConsole:       true,
 						EnableFlowCollector: true,
-						RunAsUser:           runAsUser,
+						RunAsUser:           getRunAsUserOrDefault("1000", pub),
 						RunAsGroup:          "2000",
 					},
 					// skupper status - verify initialized as interior
diff --git a/test/integration/examples/custom/helloworld/main_test.go b/test/integration/examples/custom/helloworld/main_test.go
index ae7e9dc6..cea68ae7 100644
--- a/test/integration/examples/custom/helloworld/main_test.go
+++ b/test/integration/examples/custom/helloworld/main_test.go
@@ -1,10 +1,15 @@
 package helloworld
 
 import (
+	"context"
+	"log"
 	"os"
+	"strconv"
+	"strings"
 	"testing"
 
 	"github.com/skupperproject/skupper/test/utils/base"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 // TestMain initializes flag parsing
@@ -12,3 +17,37 @@ func TestMain(m *testing.M) {
 	base.ParseFlags()
 	os.Exit(m.Run())
 }
+
+// Receives a default value, that will be overridden when running on Openshift.
+// Returns the value to be used.
+//
+// OpenShift requires RunAsUser to be configured according to annotations
+// present on the namespace, lest it will fail with SCC errors.
+//
+// If any errors found while tring to determine the correct user, this function
+// will ignore them and simply use the default value.
+func getRunAsUserOrDefault(runAsUser string, cctx *base.ClusterContext) string {
+	// OpenShift requires container user IDs to exist within a range; we try to satisfy it here.
+	namespace, err := cctx.VanClient.KubeClient.CoreV1().Namespaces().Get(context.Background(), cctx.Namespace, metav1.GetOptions{})
+	if err != nil {
+		log.Printf("Unable to get namespace %q; using pre-defined runAsUser value %v", cctx.Namespace, runAsUser)
+	} else {
+		ns_annotations := namespace.GetAnnotations()
+		if users, ok := ns_annotations["openshift.io/sa.scc.uid-range"]; ok {
+			log.Printf("OpenShift UID range annotation found: %q", users)
+			// format is like 1000860000/10000, where the first number is the
+			// range start, and the second its length
+			split_users := strings.Split(users, "/")
+			if split_users[0] != "" {
+				if _, err := strconv.Atoi(split_users[0]); err == nil {
+					runAsUser = split_users[0]
+				} else {
+					log.Printf("Failed to parse openshift uid-range annotation: using default value %v", runAsUser)
+				}
+			} else {
+				log.Printf("openshift uid-range annotation is empty, which is unexpected: using default value %v", runAsUser)
+			}
+		} // if annotation not found, we're not on Openshift, and we can use the default value.
+	}
+	return runAsUser
+}
diff --git a/test/integration/examples/http/http.go b/test/integration/examples/http/http.go
index c4204339..8fe4d259 100644
--- a/test/integration/examples/http/http.go
+++ b/test/integration/examples/http/http.go
@@ -6,8 +6,10 @@ import (
 	"regexp"
 	"strings"
 	"testing"
+	"time"
 
 	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/pkg/utils"
 	"github.com/skupperproject/skupper/test/utils/base"
 	"github.com/skupperproject/skupper/test/utils/constants"
 	"github.com/skupperproject/skupper/test/utils/k8s"
@@ -16,6 +18,7 @@ import (
 	appsv1 "k8s.io/api/apps/v1"
 	batchv1 "k8s.io/api/batch/v1"
 	apiv1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
@@ -91,7 +94,7 @@ var nginxDep = &appsv1.Deployment{
 				Containers: []apiv1.Container{
 					{
 						Name:            "nginx1",
-						Image:           "nginxinc/nginx-unprivileged:stable-alpine",
+						Image:           "quay.io/skupper/nginx-unprivileged:stable-alpine",
 						ImagePullPolicy: apiv1.PullIfNotPresent,
 						Ports: []apiv1.ContainerPort{
 							{
@@ -142,7 +145,7 @@ var nghttp2Dep = &appsv1.Deployment{
 				Containers: []apiv1.Container{
 					{
 						Name:            "nghttp2",
-						Image:           "docker.io/svagi/nghttp2",
+						Image:           "quay.io/skupper/nghttp2",
 						ImagePullPolicy: apiv1.PullIfNotPresent,
 						Ports: []apiv1.ContainerPort{
 							{
@@ -204,7 +207,7 @@ var nghttp2TlsDep = &appsv1.Deployment{
 				Containers: []apiv1.Container{
 					{
 						Name:            "nghttp2tls",
-						Image:           "docker.io/svagi/nghttp2",
+						Image:           "quay.io/skupper/nghttp2",
 						ImagePullPolicy: apiv1.PullIfNotPresent,
 						Ports: []apiv1.ContainerPort{
 							{
@@ -266,7 +269,7 @@ var nghttp2TcpTlsDep = &appsv1.Deployment{
 				Containers: []apiv1.Container{
 					{
 						Name:            "nghttp2tcptls",
-						Image:           "docker.io/svagi/nghttp2",
+						Image:           "quay.io/skupper/nghttp2",
 						ImagePullPolicy: apiv1.PullIfNotPresent,
 						Ports: []apiv1.ContainerPort{
 							{
@@ -328,7 +331,7 @@ var nghttp1TlsDep = &appsv1.Deployment{
 				Containers: []apiv1.Container{
 					{
 						Name:            "nghttp1tls",
-						Image:           "nginxinc/nginx-unprivileged:stable-alpine",
+						Image:           "quay.io/skupper/nginx-unprivileged:stable-alpine",
 						ImagePullPolicy: apiv1.PullIfNotPresent,
 						Ports: []apiv1.ContainerPort{
 							{
@@ -389,7 +392,7 @@ var nghttp2TlsDepWithCertFiles = &appsv1.Deployment{
 				Containers: []apiv1.Container{
 					{
 						Name:            "nghttp2tls",
-						Image:           "docker.io/svagi/nghttp2",
+						Image:           "quay.io/skupper/nghttp2",
 						ImagePullPolicy: apiv1.PullIfNotPresent,
 						Ports: []apiv1.ContainerPort{
 							{
@@ -461,7 +464,7 @@ var nghttp2tcpTlsDepWithCertFiles = &appsv1.Deployment{
 				Containers: []apiv1.Container{
 					{
 						Name:            "nghttp2tcptls",
-						Image:           "docker.io/svagi/nghttp2",
+						Image:           "quay.io/skupper/nghttp2",
 						ImagePullPolicy: apiv1.PullIfNotPresent,
 						Ports: []apiv1.ContainerPort{
 							{
@@ -572,7 +575,7 @@ var nghttp1TlsDepWithCertFiles = &appsv1.Deployment{
 				Containers: []apiv1.Container{
 					{
 						Name:            "nghttp1tls",
-						Image:           "nginxinc/nginx-unprivileged:stable-alpine",
+						Image:           "quay.io/skupper/nginx-unprivileged:stable-alpine",
 						ImagePullPolicy: apiv1.PullIfNotPresent,
 						Ports: []apiv1.ContainerPort{
 							{
@@ -608,7 +611,7 @@ var h2loadJob = &batchv1.Job{
 				Containers: []apiv1.Container{
 					{
 						Name:  "h2load",
-						Image: "docker.io/svagi/nghttp2",
+						Image: "quay.io/skupper/nghttp2",
 						// Command: []string{"h2load", "-n10", "-c10", "-m10", "http://nghttp2:8443"},
 						Command: []string{"h2load", "-n1000", "-c1", "-m1", "http://nghttp2:8443"},
 						Env: []apiv1.EnvVar{
@@ -650,6 +653,73 @@ var h1HeyBaseJob = &batchv1.Job{
 	},
 }
 
+// This is required to avoid test flakiness while #1208 is not fixed.  It will wait for the
+// secret to be created up to a maximum time.
+//
+// It returns nothing and ignores any errors that occur, leaving any inconsistencies to be
+// detected later on by other code
+func waitSecret(cctx *base.ClusterContext, secretName string) {
+	// The check below is required while https://github.com/skupperproject/skupper/issues/1208
+	// is not fixed; both the ServiceInterfaceCreate above and ServiceInterfaceBind below may
+	// create the secret, and sometimes they race on it, making the test flaky.
+	err := utils.Retry(
+		time.Second*3,
+		20,
+		func() (bool, error) {
+			_, err := cctx.VanClient.KubeClient.CoreV1().Secrets(cctx.Namespace).Get(
+				context.Background(),
+				secretName,
+				metav1.GetOptions{},
+			)
+			if err == nil {
+				return true, nil
+			}
+			if errors.IsNotFound(err) {
+				fmt.Printf("TLS Secret %q not yet created.  See #1208\n", secretName)
+				return false, nil
+			}
+			return false, err
+		},
+	)
+	if err != nil {
+		fmt.Printf("Secret retrieval failed: %v\n", err)
+		// We do not stop for this, as this test is just to avoid flakiness on the
+		// test; if whatever failed above is reason for an actual test failure,
+		// it will fail on the continuation of the actual test, below
+	}
+}
+
+// Updating a deployment may cause Skupper to trigger an asynchronous operation that updates
+// the configmap skupper-internal.  That may happen at the same time as another synchronous
+// operation down the road, causing it to fail (see #1153).
+//
+// This function executes the requested operation, but it first gets the ResourceVersion of
+// the configmap, and then waits until it changes, or a maximum wait time is reached.
+func updateDeployment(cctx *base.ClusterContext, ctx context.Context, deploy *appsv1.Deployment) error {
+	orig, err := cctx.VanClient.KubeClient.CoreV1().ConfigMaps(cctx.Namespace).Get(ctx, "skupper-internal", metav1.GetOptions{})
+	if err != nil {
+		return err
+	}
+	_, err = cctx.VanClient.KubeClient.AppsV1().Deployments(cctx.Namespace).Update(ctx, deploy, metav1.UpdateOptions{})
+	if err != nil {
+		return err
+	}
+	err = utils.Retry(
+		time.Millisecond*600,
+		100,
+		func() (bool, error) {
+			cm, err := cctx.VanClient.KubeClient.CoreV1().ConfigMaps(cctx.Namespace).Get(ctx, "skupper-internal", metav1.GetOptions{})
+			if err != nil {
+				fmt.Printf("Failed getting config map skupper-internal; ignoring (%s)\n", err)
+			}
+			changed := cm.ResourceVersion != orig.ResourceVersion
+			return changed, nil
+		},
+	)
+
+	return err
+}
+
 func runHeyTesWithParameter(t *testing.T, cluster *base.ClusterContext, numOfWorkers string, durationOfTests string, jobName string, targetURL string) {
 
 	waitJob := func(cc *base.ClusterContext, jobName string) {
@@ -766,7 +836,7 @@ func runTests(t *testing.T, r base.ClusterTestRunner) {
 		assert.Assert(t, err)
 	}
 
-	waitJob := func(cc *base.ClusterContext, jobName string) {
+	waitJob := func(cc *base.ClusterContext, jobName string, t *testing.T) {
 		t.Helper()
 		job, err := k8s.WaitForJob(cc.Namespace, cc.VanClient.KubeClient, jobName, constants.ImagePullingAndResourceCreationTimeout)
 		_, _ = cc.KubectlExec("logs job/" + jobName)
@@ -777,31 +847,31 @@ func runTests(t *testing.T, r base.ClusterTestRunner) {
 	// Send GET requests via HTTPD1
 	t.Run("http1", func(t *testing.T) {
 		runJob(pubCluster1, "http1", "TestHttpJob")
-		waitJob(pubCluster1, "http1")
+		waitJob(pubCluster1, "http1", t)
 	})
 
 	// Send GET requests via HTTPD2
 	t.Run("http2", func(t *testing.T) {
 		runJob(pubCluster1, "http2", "TestHttp2Job")
-		waitJob(pubCluster1, "http2")
+		waitJob(pubCluster1, "http2", t)
 	})
 
 	// Send GET requests via HTTP2 over TLS
 	t.Run("http2tls", func(t *testing.T) {
 		runJob(pubCluster1, "http2tls", "TestHttp2TlsJob")
-		waitJob(pubCluster1, "http2tls")
+		waitJob(pubCluster1, "http2tls", t)
 	})
 
 	// Send GET requests via HTTP2 over TLS on a service exposed with TCP protocol
 	t.Run("http2tcptls", func(t *testing.T) {
 		runJob(pubCluster1, "http2tcptls", "TestHttp2TcpTlsJob")
-		waitJob(pubCluster1, "http2tcptls")
+		waitJob(pubCluster1, "http2tcptls", t)
 	})
 
 	// Send GET requests via HTTP1 over TLS
 	t.Run("http1tls", func(t *testing.T) {
 		runJob(pubCluster1, "http1tls", "TestHttp1TlsJob")
-		waitJob(pubCluster1, "http1tls")
+		waitJob(pubCluster1, "http1tls", t)
 	})
 
 	// Send a huge load for HTTPD2
@@ -809,7 +879,7 @@ func runTests(t *testing.T, r base.ClusterTestRunner) {
 		jobsClient := pubCluster1.VanClient.KubeClient.BatchV1().Jobs(pubCluster1.Namespace)
 		_, err = jobsClient.Create(context.TODO(), h2loadJob, metav1.CreateOptions{})
 		assert.Assert(t, err)
-		waitJob(pubCluster1, "h2load")
+		waitJob(pubCluster1, "h2load", t)
 
 		_output, err := pubCluster1.KubectlExec("logs job/" + "h2load")
 		assert.Assert(t, err)
@@ -866,6 +936,8 @@ func setup(ctx context.Context, t *testing.T, r base.ClusterTestRunner) {
 	// Create the deployment for HTTP1 with TLS enabled
 	createDeploymentInPrivateSite(nghttp1TlsDep)
 
+	fmt.Println("Creating and binding skupper service interfaces...")
+
 	err = prv1Cluster.VanClient.ServiceInterfaceCreate(ctx, &service)
 	assert.Assert(t, err)
 
@@ -881,18 +953,25 @@ func setup(ctx context.Context, t *testing.T, r base.ClusterTestRunner) {
 	err = prv1Cluster.VanClient.ServiceInterfaceCreate(ctx, &http2TlsService)
 	assert.Assert(t, err)
 
+	waitSecret(prv1Cluster, http2TlsService.TlsCredentials)
+
 	err = prv1Cluster.VanClient.ServiceInterfaceBind(ctx, &http2TlsService, "deployment", "nghttp2tls", map[int]int{}, "")
 	assert.Assert(t, err)
 
 	//update tls service with cert files
-	_, err = prv1Cluster.VanClient.KubeClient.AppsV1().Deployments(prv1Cluster.Namespace).Update(ctx, nghttp2TlsDepWithCertFiles, metav1.UpdateOptions{})
+	err = updateDeployment(prv1Cluster, ctx, nghttp2TlsDepWithCertFiles)
 	assert.Assert(t, err)
 
+	err = prv1Cluster.VanClient.ServiceInterfaceCreate(ctx, &http2TcpTlsService)
+	assert.Assert(t, err)
+
+	waitSecret(prv1Cluster, http2TcpTlsService.TlsCredentials)
+
 	err = prv1Cluster.VanClient.ServiceInterfaceBind(ctx, &http2TcpTlsService, "deployment", "nghttp2tcptls", map[int]int{}, "")
 	assert.Assert(t, err)
 
 	//update tls service with cert files
-	_, err = prv1Cluster.VanClient.KubeClient.AppsV1().Deployments(prv1Cluster.Namespace).Update(context.TODO(), nghttp2tcpTlsDepWithCertFiles, metav1.UpdateOptions{})
+	err = updateDeployment(prv1Cluster, ctx, nghttp2tcpTlsDepWithCertFiles)
 	assert.Assert(t, err)
 
 	_, err = prv1Cluster.VanClient.KubeClient.CoreV1().ConfigMaps(prv1Cluster.Namespace).Create(context.TODO(), nghttp1TlsConfigMap, metav1.CreateOptions{})
@@ -901,11 +980,13 @@ func setup(ctx context.Context, t *testing.T, r base.ClusterTestRunner) {
 	err = prv1Cluster.VanClient.ServiceInterfaceCreate(ctx, &http1TlsService)
 	assert.Assert(t, err)
 
+	waitSecret(prv1Cluster, http1TlsService.TlsCredentials)
+
 	err = prv1Cluster.VanClient.ServiceInterfaceBind(ctx, &http1TlsService, "deployment", "nghttp1tls", map[int]int{}, "")
 	assert.Assert(t, err)
 
 	//update tls service with cert files
-	_, err = prv1Cluster.VanClient.KubeClient.AppsV1().Deployments(prv1Cluster.Namespace).Update(context.TODO(), nghttp1TlsDepWithCertFiles, metav1.UpdateOptions{})
+	err = updateDeployment(prv1Cluster, ctx, nghttp1TlsDepWithCertFiles)
 	assert.Assert(t, err)
 
 	http21service := types.ServiceInterface{
diff --git a/test/integration/examples/tls_t/job/tls_job_test.go b/test/integration/examples/tls_t/job/tls_job_test.go
index 19d81d34..adf9b88f 100644
--- a/test/integration/examples/tls_t/job/tls_job_test.go
+++ b/test/integration/examples/tls_t/job/tls_job_test.go
@@ -1,4 +1,5 @@
-//+build job
+//go:build job
+// +build job
 
 package job
 
@@ -13,14 +14,14 @@ import (
 
 func TestTlsJob(t *testing.T) {
 
+	m := regexp.MustCompile("[ /]")
 	for _, test := range tls_t.Tests {
 		testName := fmt.Sprintf("server-%v-client-%v", test.Server.Options, strings.Join(test.Client.Options, "-"))
-		m, _ := regexp.Compile("[ /]")
 		testName = m.ReplaceAllLiteralString(testName, "-")
 
 		t.Run(testName, func(t *testing.T) {
 			addr := fmt.Sprintf("ssl-server:%v", test.Server.Port)
-			result := tls_t.SendReceive(addr, test.Client.Options, test.Seek)
+			result := tls_t.SendReceive(addr, test.Client.Options, test.Seek, testName)
 			t.Logf("Success expected: %t; result: %v", test.Success, result)
 			if (result == nil) != test.Success {
 				t.Errorf("failed: client options: %v, server options: %v, result: %v", test.Client.Options, test.Server.Options, result)
diff --git a/test/integration/examples/tls_t/tls_t.go b/test/integration/examples/tls_t/tls_t.go
index c636fda1..7ed5ef5e 100644
--- a/test/integration/examples/tls_t/tls_t.go
+++ b/test/integration/examples/tls_t/tls_t.go
@@ -20,10 +20,12 @@ import (
 	"log"
 	"os"
 	"os/exec"
+	"regexp"
 	"strings"
 	"testing"
 	"time"
 
+	"github.com/skupperproject/skupper/pkg/utils"
 	"github.com/skupperproject/skupper/test/utils/base"
 	"github.com/skupperproject/skupper/test/utils/constants"
 	"github.com/skupperproject/skupper/test/utils/k8s"
@@ -34,6 +36,7 @@ import (
 	appsv1 "k8s.io/api/apps/v1"
 
 	apiv1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
@@ -489,14 +492,41 @@ func setup(ctx context.Context, t *testing.T, r base.ClusterTestRunner) {
 		fmt.Printf(" * %s (%d replicas)\n", d.Name, *d.Spec.Replicas)
 	}
 
+	// The check below is required while https://github.com/skupperproject/skupper/issues/1208
+	// is not fixed; both the ServiceInterfaceCreate above and ServiceInterfaceBind below may
+	// create the secret, and sometimes they race on it, making the test flaky.
+	err = utils.Retry(
+		time.Second*3,
+		20,
+		func() (bool, error) {
+			_, err := pub1Cluster.VanClient.KubeClient.CoreV1().Secrets(pub1Cluster.Namespace).Get(
+				context.Background(),
+				"skupper-tls-ssl-server",
+				metav1.GetOptions{},
+			)
+			if err == nil {
+				return true, nil
+			}
+			if errors.IsNotFound(err) {
+				fmt.Println("TLS Secret not yet created.  See #1208")
+				return false, nil
+			}
+			return false, err
+		},
+	)
+	if err != nil {
+		fmt.Printf("Secret retrieval failed: %v\n", err)
+		// We do not stop for this, as this test is just to avoid flakiness on the
+		// test; if whatever failed above is reason for an actual test failure,
+		// it will fail on the continuation of the actual test, below
+	}
+
 	err = pub1Cluster.VanClient.ServiceInterfaceBind(ctx, &service, "deployment", "ssl-server", map[int]int{}, "")
 	assert.Assert(t, err)
 }
 
 func runTests(t *testing.T, r base.ClusterTestRunner) {
 
-	endTime := time.Now().Add(constants.ImagePullingAndResourceCreationTimeout)
-
 	pub1Cluster, err := r.GetPublicContext(1)
 	assert.Assert(t, err)
 
@@ -522,24 +552,26 @@ func runTests(t *testing.T, r base.ClusterTestRunner) {
 	_, err = k8s.CreateTestJobWithSecret(prv1Cluster.Namespace, prv1Cluster.VanClient.KubeClient, jobName, jobCmd, "skupper-tls-ssl-server")
 	assert.Assert(t, err)
 
-	endTime = time.Now().Add(constants.ImagePullingAndResourceCreationTimeout)
+	endTime := time.Now().Add(constants.ImagePullingAndResourceCreationTimeout)
+
+	slashN := regexp.MustCompile("\n")
 
 	job, err := k8s.WaitForJob(pub1Cluster.Namespace, pub1Cluster.VanClient.KubeClient, jobName, endTime.Sub(time.Now()))
-	if err != nil || job.Status.Succeeded != 1 {
+	if err != nil || job.Status.Succeeded != 1 || testing.Verbose() {
 		logs, _ := k8s.GetJobsLogs(pub1Cluster.Namespace, pub1Cluster.VanClient.KubeClient, jobName, true)
-		log.Printf("%s job output: %s", jobName, logs)
+		log.Printf("%s pub job output: \n%s", jobName, slashN.ReplaceAllString(logs, "\n\t"))
+		pub1Cluster.KubectlExec("logs --prefix=true job/" + jobName)
 	}
 	assert.Assert(t, err)
-	pub1Cluster.KubectlExec("logs job/" + jobName)
 	k8s.AssertJob(t, job)
 
 	job, err = k8s.WaitForJob(prv1Cluster.Namespace, prv1Cluster.VanClient.KubeClient, jobName, endTime.Sub(time.Now()))
-	if err != nil || job.Status.Succeeded != 1 {
+	if err != nil || job.Status.Succeeded != 1 || testing.Verbose() {
 		logs, _ := k8s.GetJobsLogs(prv1Cluster.Namespace, prv1Cluster.VanClient.KubeClient, jobName, true)
-		log.Printf("%s job output: %s", jobName, logs)
+		log.Printf("%s prv job output: \n%s", jobName, slashN.ReplaceAllString(logs, "\n\t"))
+		prv1Cluster.KubectlExec("logs --prefix=true job/" + jobName)
 	}
 	assert.Assert(t, err)
-	prv1Cluster.KubectlExec("logs job/" + jobName)
 	k8s.AssertJob(t, job)
 
 }
@@ -552,8 +584,8 @@ func runTests(t *testing.T, r base.ClusterTestRunner) {
 // complete.
 // 'seek', if given, is a string to be searched for on the initial flush.  If
 // given and not found, return an error.
-func flushStdOut(outputCh <-chan string, seek string) error {
-	log.Printf("Flushing stdout")
+func flushStdOut(outputCh <-chan string, seek string, name string) error {
+	log.Printf("%v: Flushing stdout", name)
 
 	var found bool
 
@@ -562,27 +594,36 @@ func flushStdOut(outputCh <-chan string, seek string) error {
 	}
 
 	var count int
+	var timeoutCh <-chan time.Time
+	// Wait up to 5 seconds for the first line...
+	firstReadTimeout := time.After(time.Second * 5)
 
 outer:
 	for {
 		count++
-		timeoutCh := time.After(time.Second)
 		var line string
 		var ok bool
 
 		select {
+		case <-firstReadTimeout:
+			return fmt.Errorf("no command output before initial timeout")
 		case line, ok = <-outputCh:
+			// If any lines read, ignore initial timeout; we'll do a per-line max 1 second below
+			firstReadTimeout = nil
 		case <-timeoutCh:
-			log.Printf("Flush complete")
+			// We read at least one line, and the inter-line timeout expired.  Let's call it done
+			log.Printf("%v: Flush complete", name)
 			break outer
 		}
 		if !ok {
 			return fmt.Errorf("command output finished during initial stdout flush")
 		}
-		log.Printf("  %v", line)
+		log.Printf("%v:  %v", name, line)
 		if strings.Contains(line, seek) {
 			found = true
 		}
+		// If we got to this point, wait up to one second for the next line
+		timeoutCh = time.After(time.Second)
 	}
 
 	if !found {
@@ -600,9 +641,9 @@ outer:
 // s_client can be given in `options`.  If provided, the initial
 // output will be searched for the `seek` string.  If that is not
 // found, the function fails.
-func SendReceive(addr string, options []string, seek string) error {
+func SendReceive(addr string, options []string, seek string, name string) error {
 	defer func() {
-		log.Println("SendReceive completed")
+		log.Printf("%v: SendReceive completed", name)
 	}()
 	cmdArgs := []string{
 		"s_client",
@@ -631,8 +672,8 @@ func SendReceive(addr string, options []string, seek string) error {
 
 		strEcho := "Halo\n"
 
-		log.Println("Starting openssl s_client...")
-		log.Printf("Executing command openssl %v", cmdArgs)
+		log.Printf("%v: Starting openssl s_client...", name)
+		log.Printf("%v: Executing command openssl %v", name, cmdArgs)
 		cmd := exec.Command("openssl", cmdArgs...)
 		// We're not processing stderr, so just send it to the main stderr;
 		// we'll get it on the logs, then.
@@ -656,14 +697,14 @@ func SendReceive(addr string, options []string, seek string) error {
 			doneCh <- fmt.Errorf("error starting command: %w", err)
 		}
 		defer func() {
-			log.Printf("Closing stdin pipe...")
+			log.Printf("%v: Closing stdin pipe...", name)
 			pipeIn.Close()
-			log.Printf("Waiting for the command to complete...")
+			log.Printf("%v: Waiting for the command to complete...", name)
 			err := cmd.Wait()
 			if err != nil {
-				log.Printf("After wait, the command returned an error: %v", err)
+				log.Printf("%v: After wait, the command returned an error: %v", name, err)
 			}
-			log.Printf("...done")
+			log.Printf("%v: ...done", name)
 		}()
 
 		pReader := bufio.NewReader(pipeOut)
@@ -673,37 +714,37 @@ func SendReceive(addr string, options []string, seek string) error {
 				line, err := pReader.ReadString('\n')
 				outputCh <- line
 				if err == io.EOF {
-					log.Printf("Read EOF")
+					log.Printf("%v: Read EOF", name)
 					close(outputCh)
 					return
 				}
 				if err != nil {
-					log.Printf("non-EOF error reading command output: %v", err)
+					log.Printf("%v: non-EOF error reading command output: %v", name, err)
 					close(outputCh)
 					return
 				}
 			}
 		}()
 
-		err = flushStdOut(outputCh, seek)
+		err = flushStdOut(outputCh, seek, name)
 		if err != nil {
 			doneCh <- err
 			return
 		}
 
-		log.Println("Sending data")
+		log.Printf("%v: Sending data", name)
 		_, err = pipeIn.Write([]byte(strEcho))
 		if err != nil {
 			doneCh <- fmt.Errorf("write to server failed: %w", err)
 			return
 		}
 
-		log.Println("Receiving reply")
+		log.Printf("%v: Receiving reply", name)
 
 		reply := <-outputCh
 
-		log.Printf("Sent to server = %q", strEcho)
-		log.Printf("Reply from server = %q", string(reply))
+		log.Printf("%v: Sent to server = %q", name, strEcho)
+		log.Printf("%v: Reply from server = %q", name, string(reply))
 
 		if len(reply) == len(strEcho) {
 			// We're using openssl s_server -rev, so we have to revert
@@ -713,12 +754,12 @@ func SendReceive(addr string, options []string, seek string) error {
 				r := reply[i_r]
 
 				if r != c {
-					doneCh <- fmt.Errorf("response from server different than expected: %s (%d/%q != %d/%q)", string(reply), i_r, r, i_c, c)
+					doneCh <- fmt.Errorf("response from server different than expected: %q (%d/%q != %d/%q)", string(reply), i_r, r, i_c, c)
 				}
 			}
 		} else {
 
-			doneCh <- fmt.Errorf("response length from server different than expected: %s", string(reply))
+			doneCh <- fmt.Errorf("response length from server different than expected: %q", string(reply))
 		}
 
 		doneCh <- nil
@@ -731,9 +772,11 @@ func SendReceive(addr string, options []string, seek string) error {
 	case err = <-doneCh:
 	case <-timeoutCh:
 		err = fmt.Errorf("timed out waiting for SendReceive function to finish")
+		// If we got a timeout, we renew the timer for the stdout flushing below
+		timeoutCh = time.After(time.Minute / 2)
 	}
 
-	log.Print("Flushing stdout after test")
+	log.Printf("%v: Flushing stdout after test", name)
 	// Otherwise, output gets mixed with the next test
 	for {
 		select {
@@ -742,12 +785,14 @@ func SendReceive(addr string, options []string, seek string) error {
 				// We're done here, so we just return whatever err we got before
 				return err
 			}
-			log.Printf("  %v", line)
+			log.Printf("%v:  %v", name, line)
 		case <-timeoutCh:
 			if err != nil {
-				log.Printf("timed out waiting for SendReceive, but received an error befor: %v", err)
+				return fmt.Errorf("%v: timed out on post-test stdout flush, but received an error before: %w", name, err)
 			}
-			return fmt.Errorf("timed out waiting for SendReceive function to finish")
+			// We do not consider this as an error, as it is post-test, but worthy to note
+			log.Printf("%v: timed out on post-test stdout flush", name)
+			return nil
 		}
 	}
 }
diff --git a/test/integration/performance/amqp_test.go b/test/integration/performance/amqp_test.go
index b98fe22e..d89e7300 100644
--- a/test/integration/performance/amqp_test.go
+++ b/test/integration/performance/amqp_test.go
@@ -177,7 +177,7 @@ func getAmqpClientInfo(settings *amqpSettings) *common.ClientInfo {
 
 func getAmqpJobs(settings *amqpSettings) []common.JobInfo {
 	var jobs []common.JobInfo
-	image := "ssorj/quiver"
+	image := "quay.io/ssorj/quiver"
 	jobName := fmt.Sprintf("quiver")
 	labels := map[string]string{"job": jobName}
 	job := &batchv1.Job{
diff --git a/test/integration/performance/http_test.go b/test/integration/performance/http_test.go
index 43370925..271f7aea 100644
--- a/test/integration/performance/http_test.go
+++ b/test/integration/performance/http_test.go
@@ -426,7 +426,7 @@ func getHttpServerInfo(settings *httpSettings) *common.ServerInfo {
 
 func getHttpDeployment() *appsv1.Deployment {
 	dep, _ := k8s.NewDeployment("http-server", "", k8s.DeploymentOpts{
-		Image:  "nginxinc/nginx-unprivileged:stable-alpine",
+		Image:  "quay.io/skupper/nginx-unprivileged:stable-alpine",
 		Labels: map[string]string{"app": "http-server"},
 	})
 	return dep
diff --git a/test/integration/performance/postgres_test.go b/test/integration/performance/postgres_test.go
index 457ed2ff..1d877f93 100644
--- a/test/integration/performance/postgres_test.go
+++ b/test/integration/performance/postgres_test.go
@@ -187,7 +187,7 @@ func getPostgresClientInfo(settings *postgresSettings) *common.ClientInfo {
 
 func getPostgresJobs(settings *postgresSettings) []common.JobInfo {
 	var jobs []common.JobInfo
-	image := "postgres"
+	image := "quay.io/skupper/postgres"
 	for _, clients := range settings.clients {
 		jobName := fmt.Sprintf("pgbench-clients-%d", clients)
 		labels := map[string]string{"job": jobName}
diff --git a/test/integration/performance/redis_test.go b/test/integration/performance/redis_test.go
index 90c4ddb6..199a7d6d 100644
--- a/test/integration/performance/redis_test.go
+++ b/test/integration/performance/redis_test.go
@@ -218,7 +218,7 @@ func getRedisJobs(settings *redisSettings) []common.JobInfo {
 	var jobs []common.JobInfo
 	var testCount int
 	var found bool
-	image := "redis"
+	image := "quay.io/skupper/redis"
 	for _, clients := range settings.clients {
 		testNameCount := map[string]int{}
 		for _, testName := range settings.tests {
@@ -283,7 +283,7 @@ func getRedisServerInfo(settings *redisSettings) *common.ServerInfo {
 
 func getRedisDeployment() *appsv1.Deployment {
 	dep, _ := k8s.NewDeployment("redis-server", "", k8s.DeploymentOpts{
-		Image:  "redis",
+		Image:  "quay.io/skupper/redis",
 		Labels: map[string]string{"app": "redis-server"},
 	})
 	return dep
diff --git a/test/integration/performance/redis_tls_test.go b/test/integration/performance/redis_tls_test.go
index bf11d6dd..665a6dac 100644
--- a/test/integration/performance/redis_tls_test.go
+++ b/test/integration/performance/redis_tls_test.go
@@ -108,7 +108,7 @@ func getRedisTlsJobs(settings *redisSettings) []common.JobInfo {
 	var jobs []common.JobInfo
 	var testCount int
 	var found bool
-	image := "redis"
+	image := "quay.io/skupper/redis"
 	for _, clients := range settings.clients {
 		testNameCount := map[string]int{}
 		for _, testName := range settings.tests {
@@ -215,7 +215,7 @@ func getRedisTlsDeployment() *appsv1.Deployment {
 		"redis-server-tls",
 		"",
 		k8s.DeploymentOpts{
-			Image:  "redis",
+			Image:  "quay.io/skupper/redis",
 			Labels: map[string]string{"app": "redis-server-tls"},
 			Args: []string{
 				"redis-server",
diff --git a/test/utils/base/cluster_test_runner.go b/test/utils/base/cluster_test_runner.go
index 07c874a5..3ac03067 100644
--- a/test/utils/base/cluster_test_runner.go
+++ b/test/utils/base/cluster_test_runner.go
@@ -242,36 +242,43 @@ func ConnectSimplePublicPrivate(ctx context.Context, r *ClusterTestRunnerBase) e
 
 	publicSiteConfig, err := pub1Cluster.VanClient.SiteConfigCreate(context.Background(), routerCreateSpecPub)
 	if err != nil {
-		return err
+		return fmt.Errorf("error creating pub1 site: %w", err)
 	}
 
 	err = pub1Cluster.VanClient.RouterCreate(testContext, *publicSiteConfig)
 	if err != nil {
-		return err
+		return fmt.Errorf("error creating pub1 router: %w", err)
 	}
+	WaitSkupperRunning(pub1Cluster)
 
 	secretFile := "/tmp/" + r.Needs.NamespaceId + "_public_secret.yaml"
 	err = pub1Cluster.VanClient.ConnectorTokenCreateFile(ctx, types.DefaultVanName, secretFile)
 	if err != nil {
-		return err
+		return fmt.Errorf("error creating token: %w", err)
 	}
 
 	// Configure private cluster.
 	routerCreateSpecPrv.SkupperNamespace = prv1Cluster.Namespace
 	privateSiteConfig, err := prv1Cluster.VanClient.SiteConfigCreate(context.Background(), routerCreateSpecPrv)
+	if err != nil {
+		return fmt.Errorf("error creating prv1 site: %w", err)
+	}
 
 	err = prv1Cluster.VanClient.RouterCreate(testContext, *privateSiteConfig)
 	if err != nil {
-		return err
+		return fmt.Errorf("error creating prv1 router: %w", err)
 	}
+	WaitSkupperRunning(prv1Cluster)
 
 	var connectorCreateOpts types.ConnectorCreateOptions = types.ConnectorCreateOptions{
 		SkupperNamespace: prv1Cluster.Namespace,
 		Name:             "public",
 		Cost:             0,
 	}
-	_, err = prv1Cluster.VanClient.ConnectorCreateFromFile(ctx, secretFile, connectorCreateOpts)
-	return err
+	if _, err = prv1Cluster.VanClient.ConnectorCreateFromFile(ctx, secretFile, connectorCreateOpts); err != nil {
+		return fmt.Errorf("failed to create connection: %w", err)
+	}
+	return nil
 
 }
 
diff --git a/test/utils/skupper/cli/expose.go b/test/utils/skupper/cli/expose.go
index ab425ab5..d1b911a3 100644
--- a/test/utils/skupper/cli/expose.go
+++ b/test/utils/skupper/cli/expose.go
@@ -174,9 +174,6 @@ func (e *ExposeTester) validateTargets(platform types.Platform, svc types.Servic
 	found := false
 	for _, target := range svc.Targets {
 		expectedTargetName := e.TargetName
-		if e.TargetType == "service" {
-			expectedTargetName += "." + target.Namespace
-		}
 		if platform.IsKubernetes() {
 			if target.Name == expectedTargetName {
 				found = true
diff --git a/test/utils/skupper/cli/service/status.go b/test/utils/skupper/cli/service/status.go
index 7db95071..5b046ef0 100644
--- a/test/utils/skupper/cli/service/status.go
+++ b/test/utils/skupper/cli/service/status.go
@@ -332,7 +332,8 @@ func (s *StatusTester) parseBindings(stdout string) (ifaces []*types.ServiceInte
 			}
 			// Then we get the second item, that should be the name
 			pieces = strings.Split(strings.Trim(pieces[1], " "), " ")
-			if len(pieces) != 3 {
+			// namespace is not displayed for target type service
+			if len(pieces) < 2 || len(pieces) > 3 {
 				err = fmt.Errorf("Parsing failed due to unexpected target output format on line %v: %v", line, text)
 				return
 			}