Repository: skupperproject/skupper
Pull request #1346 (2024-01-08 23:09:16)

Less slow skupper init
My proposal to improve the experience starting a new skupper site. **Reduces startup time from ~25s to ~15s** *when the wind is blowing the right direction.

* added some logging to help me better understand where we are spending time
* exposed a way for the config-sync process to "Prime" its collector with phony BeaconRecords from where it anticipates the router and service controller to be listening in order to eliminate the time usually spent waiting for a beacon record.
* adjusts the timing service-controller uses to wait for the router to be ready, and the order in which the config-sync startup runs in order to start the controller/collector as soon as possible.
* adjusts the `get` command's http client to prevent it from hanging - observed in some failed tests.


I'd suggest reviewing with some skepticism. Especially regarding my attempts to learn / divine the router and site id from the config-sync container (SKUPPER_SITE_ID not set in environment.) I hit numerous issues in the integration test suites where the slightly adjusted timing caused failures and where my assumptions about the presence of the `skupper-site` CM didn't turn out to be valid.


Patch:
diff --git a/client/client_debug.go b/client/client_debug.go
index 1ffa40da..bbbeb044 100644
--- a/client/client_debug.go
+++ b/client/client_debug.go
@@ -9,11 +9,11 @@ import (
 	"os"
 	"os/exec"
 	"path/filepath"
-	"strings"
 	"time"
 
 	v1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/errors"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/apimachinery/pkg/runtime/serializer/json"
 	"k8s.io/client-go/kubernetes/scheme"
 
@@ -51,43 +51,20 @@ func writeTar(name string, data []byte, ts time.Time, tw *tar.Writer) error {
 	return nil
 }
 
-func (cli *VanClient) writeDeployment(name string, tw *tar.Writer) error {
+func writeObject(rto runtime.Object, path string, ext string, tw *tar.Writer) error {
 	var b bytes.Buffer
 	s := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme.Scheme, scheme.Scheme)
-
-	deployment, err := kube.GetDeployment(name, cli.Namespace, cli.KubeClient)
-	if errors.IsNotFound(err) {
-		return nil
-	} else if err != nil {
-		return err
-	}
-	if err = s.Encode(deployment, &b); err != nil {
+	if err := s.Encode(rto, &b); err != nil {
 		return err
 	}
-
-	return writeTar(name+"-deployment.yaml", b.Bytes(), time.Now(), tw)
-}
-
-func (cli *VanClient) writeConfigMap(name string, tw *tar.Writer) error {
-	var b bytes.Buffer
-	s := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme.Scheme, scheme.Scheme)
-
-	cm, err := kube.GetConfigMap(name, cli.Namespace, cli.KubeClient)
-	if errors.IsNotFound(err) {
-		return nil
-	} else if err != nil {
-		return err
-	}
-	if err = s.Encode(cm, &b); err != nil {
-		return err
-	}
-
-	return writeTar(name+"-configmap.yaml", b.Bytes(), time.Now(), tw)
+	return writeTar(path+ext, b.Bytes(), time.Now(), tw)
 }
 
 func (cli *VanClient) SkupperDump(ctx context.Context, tarName string, version string, kubeConfigPath string, kubeConfigContext string) (string, error) {
 	configMaps := []string{types.SiteConfigMapName, types.ServiceInterfaceConfigMap, types.TransportConfigMapName, "skupper-sasl-config", types.NetworkStatusConfigMapName, types.SiteLeaderLockName}
-	deployments := []string{"skupper-site-controller", "skupper-router", "skupper-service-controller"}
+	deployments := []string{"skupper-site-controller", "skupper-router", "skupper-service-controller", "skupper-prometheus"}
+	services := []string{"skupper", "skupper-router", "skupper-router-local", "skupper-prometheus"}
+	routes := []string{"claims", "skupper", "skupper-edge", "skupper-inter-router"}
 	qdstatFlags := []string{"-g", "-c", "-l", "-n", "-e", "-a", "-m", "-p"}
 
 	dumpFile := tarName
@@ -108,49 +85,79 @@ func (cli *VanClient) SkupperDump(ctx context.Context, tarName string, version s
 	tw := tar.NewWriter(gz)
 	defer tw.Close()
 
-	kv, err := runCommand("kubectl", "version", "--short", "--kubeconfig="+kubeConfigPath, "--context="+kubeConfigContext)
+	kv, err := runCommand("kubectl", "version", "-o", "yaml", "--kubeconfig="+kubeConfigPath, "--context="+kubeConfigContext)
+	if err == nil {
+		writeTar("/skupper-info/k8s-versions.yaml", kv, time.Now(), tw)
+	}
+
+	events, err := runCommand("kubectl", "events", "--kubeconfig="+kubeConfigPath, "--context="+kubeConfigContext)
+	if err == nil {
+		writeTar("/skupper-info/events.txt", events, time.Now(), tw)
+	}
+
+	endpoints, err := runCommand("kubectl", "get", "endpoints", "-o", "yaml", "--kubeconfig="+kubeConfigPath, "--context="+kubeConfigContext)
 	if err == nil {
-		writeTar("k8s-versions.txt", kv, time.Now(), tw)
+		writeTar("/skupper-info/endpoints.yaml", endpoints, time.Now(), tw)
 	}
 
 	if cli.RouteClient != nil {
-		ocv, err := runCommand("oc", "version", "--kubeconfig="+kubeConfigPath, "--context="+kubeConfigContext)
+		ocv, err := runCommand("oc", "version", "-o", "yaml", "--kubeconfig="+kubeConfigPath, "--context="+kubeConfigContext)
 		if err == nil {
-			writeTar("oc-versions.txt", ocv, time.Now(), tw)
+			writeTar("/skupper-info/oc-versions.yaml", ocv, time.Now(), tw)
 		}
 	}
 
-	var cversions []string
-	vir, err := cli.RouterInspect(context.Background())
+	_, err = runCommand("skupper", "version", "manifest")
 	if err == nil {
-		cversions = append(cversions, fmt.Sprintf("%-30s %s", "client version", version))
-		cversions = append(cversions, fmt.Sprintf("%-30s %s", "transport version", vir.TransportVersion))
-		cversions = append(cversions, fmt.Sprintf("%-30s %s\n", "controller version", vir.ControllerVersion))
-		writeTar("skupper-versions.txt", []byte(strings.Join(cversions, "\n")), time.Now(), tw)
+		manifest, err := os.ReadFile("./manifest.json")
+		if err == nil {
+			writeTar("/skupper-info/manifest.json", manifest, time.Now(), tw)
+			os.Remove("./manifest.json")
+		}
 	}
 
 	for i := range deployments {
-		err := cli.writeDeployment(deployments[i], tw)
+		deployment, err := cli.KubeClient.AppsV1().Deployments(cli.Namespace).Get(context.TODO(), deployments[i], metav1.GetOptions{})
+		if err != nil {
+			continue
+		}
+		err = writeObject(deployment, "/deployments/"+deployment.Name, ".yaml", tw)
 		if err != nil {
 			return dumpFile, err
 		}
 
-		component := kube.GetDeploymentLabel(deployments[i], "skupper.io/component", cli.Namespace, cli.KubeClient)
+		component, ok := deployment.Spec.Template.Labels["skupper.io/component"]
+		if !ok {
+			continue
+		}
 
-		podList, err := kube.GetPods("skupper.io/component="+component, cli.Namespace, cli.KubeClient)
-		if errors.IsNotFound(err) {
+		podList, err := cli.KubeClient.CoreV1().Pods(cli.Namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: "skupper.io/component=" + component})
+		if err != nil {
 			continue
-		} else if err != nil {
-			return dumpFile, err
 		}
-		for _, pod := range podList {
+
+		for _, pod := range podList.Items {
+			pod, err := cli.KubeClient.CoreV1().Pods(cli.Namespace).Get(context.TODO(), pod.Name, metav1.GetOptions{})
+			if err != nil {
+				continue
+			} else {
+				err := writeObject(pod, "/pods/"+pod.Name+"/pod", ".yaml", tw)
+				if err != nil {
+					return dumpFile, err
+				}
+			}
+			top, err := runCommand("kubectl", "top", "pod", pod.Name, "--kubeconfig="+kubeConfigPath, "--context="+kubeConfigContext)
+			if err == nil {
+				writeTar("/pods/"+pod.Name+"/top-pod.txt", top, time.Now(), tw)
+			}
+
 			for container := range pod.Spec.Containers {
 				if pod.Spec.Containers[container].Name == "router" {
 					// while we are here collect qdstats, logs will show these operations
 					for x := range qdstatFlags {
 						qdr, err := kube.ExecCommandInContainer([]string{"skstat", qdstatFlags[x]}, pod.Name, "router", cli.Namespace, cli.KubeClient, cli.RestConfig)
 						if err == nil {
-							writeTar(pod.Name+"-skstat"+qdstatFlags[x]+".txt", qdr.Bytes(), time.Now(), tw)
+							writeTar("/pods/"+pod.Name+"/skstat/skstat"+qdstatFlags[x]+".txt", qdr.Bytes(), time.Now(), tw)
 						} else {
 							continue
 
@@ -159,23 +166,23 @@ func (cli *VanClient) SkupperDump(ctx context.Context, tarName string, version s
 				} else if pod.Spec.Containers[container].Name == "service-controller" {
 					events, err := kube.ExecCommandInContainer([]string{"get", "events"}, pod.Name, "service-controller", cli.Namespace, cli.KubeClient, cli.RestConfig)
 					if err == nil {
-						writeTar(pod.Name+"-events.txt", events.Bytes(), time.Now(), tw)
+						writeTar("/pods/"+pod.Name+"/events/"+pod.Spec.Containers[container].Name+"-events.txt", events.Bytes(), time.Now(), tw)
 					}
 					policies, err := kube.ExecCommandInContainer([]string{"get", "policies", "list"}, pod.Name, "service-controller", cli.Namespace, cli.KubeClient, cli.RestConfig)
 					if err == nil {
-						writeTar(pod.Name+"-policies.txt", policies.Bytes(), time.Now(), tw)
+						writeTar("/pods/"+pod.Name+"/policies/"+pod.Spec.Containers[container].Name+"-policies.txt", policies.Bytes(), time.Now(), tw)
 					}
 				}
 
 				log, err := kube.GetPodContainerLogs(pod.Name, pod.Spec.Containers[container].Name, cli.Namespace, cli.KubeClient)
 				if err == nil {
-					writeTar(pod.Name+"-"+pod.Spec.Containers[container].Name+"-logs.txt", []byte(log), time.Now(), tw)
+					writeTar("/pods/"+pod.Name+"/logs/"+pod.Spec.Containers[container].Name+"-logs.txt", []byte(log), time.Now(), tw)
 				}
 
-				if hasRestartedContainer(pod) {
+				if hasRestartedContainer(*pod) {
 					prevLog, err := kube.GetPodContainerLogsWithOpts(pod.Name, pod.Spec.Containers[container].Name, cli.Namespace, cli.KubeClient, v1.PodLogOptions{Previous: true})
 					if err == nil {
-						writeTar(pod.Name+"-"+pod.Spec.Containers[container].Name+"-logs-previous.txt", []byte(prevLog), time.Now(), tw)
+						writeTar("/pods/"+pod.Name+"/logs/"+pod.Spec.Containers[container].Name+"-logs-previous.txt", []byte(prevLog), time.Now(), tw)
 					}
 				}
 
@@ -184,11 +191,37 @@ func (cli *VanClient) SkupperDump(ctx context.Context, tarName string, version s
 	}
 
 	for i := range configMaps {
-		err := cli.writeConfigMap(configMaps[i], tw)
-		if err != nil {
-			return dumpFile, err
+		cm, err := cli.KubeClient.CoreV1().ConfigMaps(cli.Namespace).Get(context.TODO(), configMaps[i], metav1.GetOptions{})
+		if err == nil {
+			err := writeObject(cm, "/configmaps/"+cm.Name, ".yaml", tw)
+			if err != nil {
+				return dumpFile, err
+			}
 		}
 	}
+
+	for i := range services {
+		service, err := cli.KubeClient.CoreV1().Services(cli.Namespace).Get(context.TODO(), services[i], metav1.GetOptions{})
+		if err == nil {
+			err := writeObject(service, "/services/"+service.Name, ".yaml", tw)
+			if err != nil {
+				return dumpFile, err
+			}
+		}
+	}
+
+	if cli.RouteClient != nil {
+		for i := range routes {
+			route, err := cli.RouteClient.Routes(cli.Namespace).Get(context.TODO(), routes[i], metav1.GetOptions{})
+			if err == nil {
+				err := writeObject(route, "/routes/"+route.Name, ".yaml", tw)
+				if err != nil {
+					return dumpFile, err
+				}
+			}
+		}
+	}
+
 	return dumpFile, nil
 }
 
diff --git a/client/container/client.go b/client/container/client.go
index 3dccc3bb..0311d974 100644
--- a/client/container/client.go
+++ b/client/container/client.go
@@ -1,6 +1,7 @@
 package container
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"os"
@@ -30,7 +31,7 @@ type Client interface {
 	ContainerRestart(id string) error
 	ImageList() ([]*Image, error)
 	ImageInspect(id string) (*Image, error)
-	ImagePull(id string) error
+	ImagePull(ctx context.Context, id string) error
 	NetworkList() ([]*Network, error)
 	NetworkInspect(id string) (*Network, error)
 	NetworkCreate(network *Network) (*Network, error)
diff --git a/client/podman/container.go b/client/podman/container.go
index c26e5fa3..ed2f5f51 100644
--- a/client/podman/container.go
+++ b/client/podman/container.go
@@ -1,6 +1,7 @@
 package podman
 
 import (
+	"context"
 	"fmt"
 	"os/user"
 	"strconv"
@@ -245,8 +246,8 @@ func (p *PodmanRestClient) ContainerUpdate(name string, fn func(newContainer *co
 }
 
 // ContainerUpdateImage updates the image used by the given container (name).
-func (p *PodmanRestClient) ContainerUpdateImage(name string, newImage string) (*container.Container, error) {
-	err := p.ImagePull(newImage)
+func (p *PodmanRestClient) ContainerUpdateImage(ctx context.Context, name string, newImage string) (*container.Container, error) {
+	err := p.ImagePull(ctx, newImage)
 	if err != nil {
 		return nil, fmt.Errorf("error pulling image %q: %s", newImage, err)
 	}
@@ -351,6 +352,7 @@ func (p *PodmanRestClient) ContainerExec(id string, command []string) (string, e
 	startParams := exec.NewExecStartLibpodParams()
 	startParams.ID = resp.ID
 
+	reader := &multiplexedBodyReader{}
 	startOp := &runtime.ClientOperation{
 		ID:                 "ExecStartLibpod",
 		Method:             "POST",
@@ -359,19 +361,20 @@ func (p *PodmanRestClient) ContainerExec(id string, command []string) (string, e
 		ConsumesMediaTypes: []string{"application/json", "application/x-tar"},
 		Schemes:            []string{"http", "https"},
 		Params:             startParams,
-		Reader:             &responseReaderBody{},
+		Reader:             reader,
 		Context:            params.Context,
 		Client:             params.HTTPClient,
 	}
 
 	restClient, ok := p.RestClient.(*runtimeclient.Runtime)
 	if ok {
-		restClient.Consumers["*/*"] = &responseReaderBody{}
+		restClient.Consumers["*/*"] = reader
 	}
 	result, err = p.RestClient.Submit(startOp)
 	if err != nil {
 		return "", fmt.Errorf("error starting execution: %v", err)
 	}
+	// stdout and stderr are also available under reader.Stdout() and reader.Stderr()
 	out, ok := result.(string)
 	if !ok {
 		return "", fmt.Errorf("error parsing response")
@@ -379,11 +382,6 @@ func (p *PodmanRestClient) ContainerExec(id string, command []string) (string, e
 	return out, nil
 }
 
-/*
-	ContainerExec(id string, command []string) (string, string, error)
-	ContainerLogs(id string) (string, error)
-*/
-
 func FromListContainer(c models.ListContainer) *container.Container {
 	ct := &container.Container{
 		ID:       c.ID,
@@ -531,6 +529,7 @@ func (p *PodmanRestClient) ContainerLogs(id string) (string, error) {
 	params.Name = id
 	params.Stdout = boolTrue()
 	params.Stderr = boolTrue()
+	reader := &responseReaderOctetStreamBody{}
 	op := &runtime.ClientOperation{
 		ID:                 "ContainerLogsLibpod",
 		Method:             "GET",
@@ -539,7 +538,7 @@ func (p *PodmanRestClient) ContainerLogs(id string) (string, error) {
 		ConsumesMediaTypes: []string{"application/json", "application/x-tar"},
 		Schemes:            []string{"http", "https"},
 		Params:             params,
-		Reader:             &responseReaderBody{},
+		Reader:             reader,
 		Context:            params.Context,
 		Client:             params.HTTPClient,
 	}
diff --git a/client/podman/image.go b/client/podman/image.go
index 59ea9450..8188f013 100644
--- a/client/podman/image.go
+++ b/client/podman/image.go
@@ -1,6 +1,7 @@
 package podman
 
 import (
+	"context"
 	"encoding/base64"
 	"encoding/json"
 	"fmt"
@@ -12,6 +13,17 @@ import (
 	"github.com/skupperproject/skupper/client/generated/libpod/client/images"
 )
 
+const (
+	imagePullRecommendation = `
+If the image is being pulled from an authenticated registry,
+make sure to log in first, using:
+
+    podman login <registry-url>
+
+In case you are using a custom authentication file, you should
+set the REGISTRY_AUTH_FILE environment variable.`
+)
+
 func (p *PodmanRestClient) ImageList() ([]*container.Image, error) {
 	cli := images.New(p.RestClient, formats)
 	param := images.NewImageListLibpodParams()
@@ -60,7 +72,7 @@ func (p *PodmanRestClient) ImageInspect(id string) (*container.Image, error) {
 	return img, nil
 }
 
-func (p *PodmanRestClient) ImagePull(id string) error {
+func (p *PodmanRestClient) ImagePull(ctx context.Context, id string) error {
 	params := images.NewImagePullLibpodParams()
 	params.Reference = &id
 	params.TLSVerify = new(bool)
@@ -73,6 +85,7 @@ func (p *PodmanRestClient) ImagePull(id string) error {
 	params.XRegistryAuth = getXRegistryAuth(id)
 
 	// Need to do that as the default response reader is being closed too soon
+	reader := &responseReaderJSONErrorBody{}
 	op := &runtime.ClientOperation{
 		ID:                 "ImagePullLibpod",
 		Method:             "POST",
@@ -81,13 +94,16 @@ func (p *PodmanRestClient) ImagePull(id string) error {
 		ConsumesMediaTypes: []string{"application/json", "application/x-tar"},
 		Schemes:            []string{"http", "https"},
 		Params:             params,
-		Reader:             &responseReaderBody{},
-		Context:            params.Context,
+		Reader:             reader,
+		Context:            ctx,
 		Client:             params.HTTPClient,
 	}
 	res, err := p.RestClient.Submit(op)
 	if err != nil {
-		return fmt.Errorf("error pulling image %s: %v", id, err)
+		return &Error{
+			Err:            fmt.Errorf("error pulling image %s: %v", id, err),
+			Recommendation: imagePullRecommendation,
+		}
 	}
 	// eventually err is nil but auth problems are reported as json after a string msg
 	if resStr, ok := res.(string); ok {
@@ -100,7 +116,10 @@ func (p *PodmanRestClient) ImagePull(id string) error {
 		var jsonRes map[string]interface{}
 		if err = json.Unmarshal([]byte(resStrClean), &jsonRes); err == nil {
 			if errMsg, ok := jsonRes["error"]; ok && errMsg != "" {
-				return fmt.Errorf("unable to pull image %s: %v", id, errMsg)
+				return &Error{
+					Err:            fmt.Errorf("unable to pull image %s: %v", id, errMsg),
+					Recommendation: imagePullRecommendation,
+				}
 			}
 		}
 	}
@@ -161,3 +180,12 @@ func getXRegistryAuth(image string) *string {
 	}
 	return nil
 }
+
+type Error struct {
+	Recommendation string
+	Err            error
+}
+
+func (e *Error) Error() string {
+	return fmt.Sprintf("%s\n\nRecommendation: %s\n", e.Err, e.Recommendation)
+}
diff --git a/client/podman/rest.go b/client/podman/rest.go
index 2328beef..c373b2ed 100644
--- a/client/podman/rest.go
+++ b/client/podman/rest.go
@@ -3,6 +3,7 @@ package podman
 import (
 	"context"
 	"crypto/tls"
+	"encoding/binary"
 	"fmt"
 	"io"
 	"net"
@@ -194,10 +195,110 @@ func (r *responseReaderID) ReadResponse(response runtime.ClientResponse, consume
 	}
 }
 
-type responseReaderBody struct {
+type multiplexedBodyReader struct {
+	output map[byte][]byte
 }
 
-func (r *responseReaderBody) Consume(reader io.Reader, i interface{}) error {
+func (r *multiplexedBodyReader) Stdout() string {
+	if r.output == nil {
+		return ""
+	}
+	return string(r.output[1])
+}
+
+func (r *multiplexedBodyReader) Stderr() string {
+	if r.output == nil {
+		return ""
+	}
+	return string(r.output[2])
+}
+
+func (r *multiplexedBodyReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
+	return ReadResponse(response, r)
+}
+
+func (r *multiplexedBodyReader) readStreams(offset *uint64, data []byte) {
+	if *offset == 0 {
+		r.output = map[byte][]byte{0: []byte{}, 1: []byte{}, 2: []byte{}}
+	}
+	if *offset == uint64(len(data)) {
+		return
+	}
+	header := data[*offset : *offset+8]
+	// stream can be: 0 = stdin, 1 = stdout, 2 = stderr
+	streamId := header[0]
+	// frame size to read
+	size := binary.BigEndian.Uint32(header[4:])
+	// skip to start of frame
+	*offset += 8
+	// frame content
+	frame := data[*offset : *offset+uint64(size)]
+	r.output[streamId] = append(r.output[streamId], frame...)
+	*offset += uint64(size)
+	r.readStreams(offset, data)
+}
+
+func (r *multiplexedBodyReader) Consume(reader io.Reader, i interface{}) error {
+	consumeErr := runtime.TextConsumer().Consume(reader, i)
+	bodyStr, ok := i.(*string)
+	if !ok {
+		return fmt.Errorf("error parsing body")
+	}
+	body := *bodyStr
+
+	//  Source: https://github.com/docker/engine/blob/8955d8da8951695a98eb7e15bead19d402c6eb27/api/swagger.yaml#L6818C4-L6818C4
+	//	### Stream format
+	//
+	//	When the TTY setting is disabled in [`POST /containers/create`](#operation/ContainerCreate),
+	//	the stream over the hijacked connected is multiplexed to separate out
+	//	`stdout` and `stderr`. The stream consists of a series of frames, each
+	//	containing a header and a payload.
+	//
+	//		The header contains the information which the stream writes (`stdout` or
+	//	`stderr`). It also contains the size of the associated frame encoded in
+	//	the last four bytes (`uint32`).
+	//
+	//		It is encoded on the first eight bytes like this:
+	//
+	//	```go
+	//        header := [8]byte{STREAM_TYPE, 0, 0, 0, SIZE1, SIZE2, SIZE3, SIZE4}
+	//        ```
+	//
+	//	`STREAM_TYPE` can be:
+	//
+	//	- 0: `stdin` (is written on `stdout`)
+	//	- 1: `stdout`
+	//	- 2: `stderr`
+	//
+	//	`SIZE1, SIZE2, SIZE3, SIZE4` are the four bytes of the `uint32` size
+	//	encoded as big endian.
+	//
+	//		Following the header is the payload, which is the specified number of
+	//	bytes of `STREAM_TYPE`.
+	//
+	//		The simplest way to implement this protocol is the following:
+	//
+	//	1. Read 8 bytes.
+	//	2. Choose `stdout` or `stderr` depending on the first byte.
+	//	3. Extract the frame size from the last four bytes.
+	//	4. Read the extracted size and output it on the correct output.
+	//	5. Goto 1.
+	offset := uint64(0)
+	r.readStreams(&offset, []byte(body))
+
+	// remove next 8 bytes if control character found (0 or 1)
+	*bodyStr = string(r.output[1]) + string(r.output[2])
+	return consumeErr
+}
+
+type responseReaderOctetStreamBody struct {
+}
+
+func (r *responseReaderOctetStreamBody) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
+	return ReadResponse(response, r)
+}
+
+func (r *responseReaderOctetStreamBody) Consume(reader io.Reader, i interface{}) error {
 	bodyStr, ok := i.(*string)
 	if !ok {
 		return fmt.Errorf("error parsing body")
@@ -208,10 +309,14 @@ func (r *responseReaderBody) Consume(reader io.Reader, i interface{}) error {
 	}
 
 	var dataClean []byte
-	for idx, v := range data {
-		// skip 8 first bytes every 8k chunk
-		if idx%(8192+8) < 8 {
+	idx := 0
+	for _, v := range data {
+		// skip 8 first bytes on each line
+		if idx < 8 {
+			idx++
 			continue
+		} else if v == '\n' {
+			idx = 0
 		}
 		dataClean = append(dataClean, v)
 	}
@@ -224,17 +329,39 @@ func (r *responseReaderBody) Consume(reader io.Reader, i interface{}) error {
 		}
 	}
 
-	// fmt.Println(string(dataClean[len(dataClean)-8 : len(dataClean)]))
-	// fmt.Println(dataClean[len(dataClean)-8 : len(dataClean)])
 	*bodyStr = string(dataClean)
 	return nil
 }
 
-func (r *responseReaderBody) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
+type responseReaderJSONErrorBody struct {
+}
+
+func (r *responseReaderJSONErrorBody) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
+	return ReadResponse(response, r)
+}
+
+func (r *responseReaderJSONErrorBody) Consume(reader io.Reader, i interface{}) error {
+	bodyStr, ok := i.(*string)
+	if !ok {
+		return fmt.Errorf("error parsing body")
+	}
+	data, err := io.ReadAll(reader)
+	if err != nil {
+		return fmt.Errorf("error reading response body: %v", err)
+	}
+
+	// identify error messages in multiline response
+	errorRegexp := regexp.MustCompile(`(?s).*({"error":\s*"[^"]+"}).*(?s)`)
+	*bodyStr = errorRegexp.ReplaceAllString(string(data), `$1`)
+	return nil
+}
+
+func ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) {
 	switch response.Code() {
 	case 200, 201:
+		var err error
 		bodyStr := ""
-		err := r.Consume(response.Body(), &bodyStr)
+		err = consumer.Consume(response.Body(), &bodyStr)
 		if err != nil {
 			return nil, err
 		}
diff --git a/client/router_update.go b/client/router_update.go
index 68240f85..6ce0be06 100644
--- a/client/router_update.go
+++ b/client/router_update.go
@@ -95,7 +95,6 @@ func (cli *VanClient) RouterUpdateVersionInNamespace(ctx context.Context, hup bo
 		return false, fmt.Errorf("Site (%s) is newer than library (%s); cannot update", site.Version, version.Version)
 	}
 	renameFor050 := false
-	updateSecretsFor102 := false
 	addClaimsSupport := false
 	addMultiportServices := false
 	addClusterPolicy := false
@@ -104,6 +103,7 @@ func (cli *VanClient) RouterUpdateVersionInNamespace(ctx context.Context, hup bo
 	substituteFlowCollector := false
 	addPrometheusServer := false
 	moveClaims := false
+	updateRole := false
 	inprogress, originalVersion, err := cli.isUpdating(namespace)
 	if err != nil {
 		return false, err
@@ -117,8 +117,10 @@ func (cli *VanClient) RouterUpdateVersionInNamespace(ctx context.Context, hup bo
 		addCertsSharedVolume = utils.LessRecentThanVersion(originalVersion, "0.9.0")
 		substituteFlowCollector = utils.LessRecentThanVersion(originalVersion, "1.3.0")
 		addPrometheusServer = utils.LessRecentThanVersion(originalVersion, "1.4.0")
-		moveClaims = utils.LessRecentThanVersion(originalVersion, "1.5.0")
-		updateSecretsFor102 = utils.LessRecentThanVersion(originalVersion, "1.0.2")
+		if utils.LessRecentThanVersion(originalVersion, "1.5.0") {
+			moveClaims = !config.IsEdge()
+			updateRole = true //config-sync requires extra permission to write skupper-network-status configmap
+		}
 	} else {
 		originalVersion = site.Version
 	}
@@ -143,7 +145,8 @@ func (cli *VanClient) RouterUpdateVersionInNamespace(ctx context.Context, hup bo
 				addPrometheusServer = true
 			}
 			if utils.LessRecentThanVersion(originalVersion, "1.5.0") {
-				moveClaims = true
+				moveClaims = !config.IsEdge()
+				updateRole = true //config-sync requires extra permission to write skupper-network-status configmap
 			}
 
 			err = cli.updateStarted(site.Version, namespace, configmap.ObjectMeta.OwnerReferences)
@@ -411,40 +414,35 @@ func (cli *VanClient) RouterUpdateVersionInNamespace(ctx context.Context, hup bo
 	}
 	updateRouter := false
 
-	if updateSecretsFor102 {
+	secret, err := cli.KubeClient.CoreV1().Secrets(namespace).Get(context.TODO(), types.LocalClientSecret, metav1.GetOptions{})
+	if err != nil {
+		return false, err
+	}
+	if !strings.EqualFold(string(secret.Data["connect.json"]), configs.ConnectJson(types.QualifiedServiceName(types.LocalTransportServiceName, namespace))) {
 
-		secret, err := cli.KubeClient.CoreV1().Secrets(namespace).Get(context.TODO(), types.LocalClientSecret, metav1.GetOptions{})
+		err = kube.DeleteSecret(types.LocalClientSecret, namespace, cli.KubeClient)
 		if err != nil {
 			return false, err
 		}
 
-		if strings.EqualFold(string(secret.Data["connect.json"]), configs.ConnectJson(types.QualifiedServiceName(types.LocalClientSecret, namespace))) {
-
-			err = kube.DeleteSecret(types.LocalClientSecret, namespace, cli.KubeClient)
-			if err != nil {
-				return false, err
-			}
-
-			credential := types.Credential{
-				CA:          types.LocalCaSecret,
-				Name:        types.LocalClientSecret,
-				Subject:     types.LocalTransportServiceName,
-				Hosts:       []string{},
-				ConnectJson: true,
-			}
-
-			var owner *metav1.OwnerReference
-			if len(configmap.ObjectMeta.OwnerReferences) > 0 {
-				owner = &configmap.ObjectMeta.OwnerReferences[0]
-			}
+		credential := types.Credential{
+			CA:          types.LocalCaSecret,
+			Name:        types.LocalClientSecret,
+			Subject:     types.LocalTransportServiceName,
+			Hosts:       []string{},
+			ConnectJson: true,
+		}
 
-			_, err = kube.NewSecret(credential, owner, namespace, cli.KubeClient)
-			if err != nil {
-				return false, err
-			}
+		var owner *metav1.OwnerReference
+		if len(configmap.ObjectMeta.OwnerReferences) > 0 {
+			owner = &configmap.ObjectMeta.OwnerReferences[0]
+		}
 
-			updateRouter = true
+		_, err = kube.NewSecret(credential, owner, namespace, cli.KubeClient)
+		if err != nil {
+			return false, err
 		}
+		updateRouter = true
 	}
 
 	if renameFor050 {
@@ -531,6 +529,11 @@ func (cli *VanClient) RouterUpdateVersionInNamespace(ctx context.Context, hup bo
 		}
 		kube.AppendSecretVolumeWithVolumeName(&router.Spec.Template.Spec.Volumes, &router.Spec.Template.Spec.Containers[1].VolumeMounts, types.SiteServerSecret, "claims-cert", "/etc/skupper-internal/")
 		updateRouter = true
+	} else if updateRole {
+		err = kube.UpdateRole(namespace, types.TransportRoleName, types.TransportPolicyRule, cli.KubeClient)
+		if err != nil {
+			return false, err
+		}
 	}
 
 	if updateRouter || updateSite || hup {
diff --git a/cmd/config-sync/collector.go b/cmd/config-sync/collector.go
index 9143bc6c..e375a4f4 100644
--- a/cmd/config-sync/collector.go
+++ b/cmd/config-sync/collector.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"context"
+	"fmt"
 	"log"
 	"os"
 	"time"
@@ -31,41 +32,62 @@ func updateLockOwner(lockname, namespace string, owner *metav1.OwnerReference, c
 	return err
 }
 
-func siteCollector(stopCh <-chan struct{}, cli *client.VanClient) *flow.FlowCollector {
+func siteCollector(stopCh <-chan struct{}, cli *client.VanClient) {
 	var fc *flow.FlowCollector
 	siteData := map[string]string{}
 	platform := config.GetPlatform()
-	if platform == "" || platform == types.PlatformKubernetes {
-		current, err := kube.GetDeployment(types.TransportDeploymentName, cli.Namespace, cli.KubeClient)
-		if err != nil {
-			log.Fatal("Failed to get transport deployment", err.Error())
-		}
-		owner := kube.GetDeploymentOwnerReference(current)
+	if platform != types.PlatformKubernetes {
+		return
+	}
+	current, err := kube.GetDeployment(types.TransportDeploymentName, cli.Namespace, cli.KubeClient)
+	if err != nil {
+		log.Fatal("Failed to get transport deployment", err.Error())
+	}
+	owner := kube.GetDeploymentOwnerReference(current)
 
-		existing, err := kube.NewConfigMap(types.NetworkStatusConfigMapName, &siteData, nil, nil, &owner, cli.Namespace, cli.KubeClient)
-		if err != nil && existing == nil {
-			log.Fatal("Failed to create site status config map ", err.Error())
-		}
+	existing, err := kube.NewConfigMap(types.NetworkStatusConfigMapName, &siteData, nil, nil, &owner, cli.Namespace, cli.KubeClient)
+	if err != nil && existing == nil {
+		log.Fatal("Failed to create site status config map ", err.Error())
+	}
 
-		err = updateLockOwner(types.SiteLeaderLockName, cli.Namespace, &owner, cli)
-		if err != nil {
-			log.Println("Update lock error", err.Error())
-		}
+	err = updateLockOwner(types.SiteLeaderLockName, cli.Namespace, &owner, cli)
+	if err != nil {
+		log.Println("Update lock error", err.Error())
+	}
 
-		fc = flow.NewFlowCollector(flow.FlowCollectorSpec{
-			Mode:              flow.RecordStatus,
-			Namespace:         cli.Namespace,
-			Origin:            os.Getenv("SKUPPER_SITE_ID"),
-			PromReg:           nil,
-			ConnectionFactory: qdr.NewConnectionFactory("amqp://localhost:5672", nil),
-			FlowRecordTtl:     time.Minute * 15})
+	fc = flow.NewFlowCollector(flow.FlowCollectorSpec{
+		Mode:              flow.RecordStatus,
+		Namespace:         cli.Namespace,
+		PromReg:           nil,
+		ConnectionFactory: qdr.NewConnectionFactory("amqp://localhost:5672", nil),
+		FlowRecordTtl:     time.Minute * 15})
+
+	go primeBeacons(fc, cli)
+	log.Println("COLLECTOR: Starting flow collector")
+	fc.Start(stopCh)
+}
 
-		fc.Start(stopCh)
+// primeBeacons attempts to guess the router and service-controller vanflow IDs
+// to pass to the flow collector in order to accelerate startup time.
+func primeBeacons(fc *flow.FlowCollector, cli *client.VanClient) {
+	podname, _ := os.Hostname()
+	var prospectRouterID string
+	if len(podname) >= 5 {
+		prospectRouterID = fmt.Sprintf("%s:0", podname[len(podname)-5:])
+	}
+	var siteID string
+	cm, err := kube.WaitConfigMapCreated(types.SiteConfigMapName, cli.Namespace, cli.KubeClient, 5*time.Second, 250*time.Millisecond)
+	if err != nil {
+		log.Printf("COLLECTOR: failed to get skupper-site ConfigMap. Proceeding without Site ID. %s\n", err)
+	} else if cm != nil {
+		siteID = string(cm.ObjectMeta.UID)
 	}
-	return fc
+	log.Printf("COLLECTOR: Priming site with expected beacons for '%s' and '%s'\n", prospectRouterID, siteID)
+	fc.PrimeSiteBeacons(siteID, prospectRouterID)
 }
 
 func runLeaderElection(lock *resourcelock.ConfigMapLock, ctx context.Context, id string, cli *client.VanClient) {
+	begin := time.Now()
 	var stopCh chan struct{}
 	podname, _ := os.Hostname()
 	leaderelection.RunOrDie(ctx, leaderelection.LeaderElectionConfig{
@@ -76,7 +98,7 @@ func runLeaderElection(lock *resourcelock.ConfigMapLock, ctx context.Context, id
 		RetryPeriod:     2 * time.Second,
 		Callbacks: leaderelection.LeaderCallbacks{
 			OnStartedLeading: func(c context.Context) {
-				log.Printf("COLLECTOR: Leader %s starting site collection \n", podname)
+				log.Printf("COLLECTOR: Leader %s starting site collection after %s\n", podname, time.Since(begin))
 				stopCh = make(chan struct{})
 				siteCollector(stopCh, cli)
 			},
diff --git a/cmd/config-sync/main.go b/cmd/config-sync/main.go
index a370968f..3bf444ea 100644
--- a/cmd/config-sync/main.go
+++ b/cmd/config-sync/main.go
@@ -72,6 +72,9 @@ func main() {
 		log.Fatal("Error waiting for router pods to be ready ", err.Error())
 	}
 
+	log.Println("CONFIG_SYNC: Starting collector...")
+	go startCollector(cli)
+
 	event.StartDefaultEventStore(stopCh)
 	if claims.StartClaimVerifier(cli.KubeClient, cli.Namespace, cli, cli) {
 		log.Println("CONFIG_SYNC: Claim verifier started")
@@ -98,8 +101,6 @@ func main() {
 	log.Println("CONFIG_SYNC: Starting sync controller loop...")
 	configSync.start(stopCh)
 
-	go startCollector(cli)
-
 	<-stopCh
 	log.Println("CONFIG_SYNC: Shutting down...")
 	configSync.stop()
diff --git a/cmd/flow-collector/handlers.go b/cmd/flow-collector/handlers.go
index 0591d1d1..07d480f1 100644
--- a/cmd/flow-collector/handlers.go
+++ b/cmd/flow-collector/handlers.go
@@ -176,12 +176,20 @@ func (c *Controller) promqueryHandler(w http.ResponseWriter, r *http.Request) {
 	proxyReq, err := http.NewRequest(r.Method, urlOut, nil)
 	if err != nil {
 		log.Printf("COLLECTOR: prom proxy request error: %s\n", err.Error())
+
+		w.WriteHeader(http.StatusInternalServerError)
+		fmt.Fprintf(w, "Internal Server Error: %s\n", err.Error())
+		return
 	}
 
 	w.Header().Set("Content-Type", "application/json")
 	proxyResp, err := client.Do(proxyReq)
 	if err != nil {
 		log.Printf("COLLECTOR: Prometheus query error: %s\n", err.Error())
+
+		w.WriteHeader(http.StatusInternalServerError)
+		fmt.Fprintf(w, "Internal Server Error: %s\n", err.Error())
+		return
 	} else {
 		w.WriteHeader(proxyResp.StatusCode)
 		data, _ := io.ReadAll(proxyResp.Body)
@@ -197,6 +205,10 @@ func (c *Controller) promqueryrangeHandler(w http.ResponseWriter, r *http.Reques
 	proxyReq, err := http.NewRequest(r.Method, urlOut, nil)
 	if err != nil {
 		log.Printf("COLLECTOR: prom proxy request error: %s \n", err.Error())
+
+		w.WriteHeader(http.StatusInternalServerError)
+		fmt.Fprintf(w, "Internal Server Error: %s\n", err.Error())
+		return
 	}
 
 	w.Header().Set("Content-Type", "application/json")
@@ -204,6 +216,10 @@ func (c *Controller) promqueryrangeHandler(w http.ResponseWriter, r *http.Reques
 	w.WriteHeader(proxyResp.StatusCode)
 	if err != nil {
 		log.Printf("COLLECTOR: Prometheus query_range error: %s\n", err.Error())
+
+		w.WriteHeader(http.StatusInternalServerError)
+		fmt.Fprintf(w, "Internal Server Error: %s\n", err.Error())
+		return
 	} else {
 		data, _ := io.ReadAll(proxyResp.Body)
 		proxyResp.Body.Close()
diff --git a/cmd/get/get.go b/cmd/get/get.go
index 693a3644..b0530e00 100644
--- a/cmd/get/get.go
+++ b/cmd/get/get.go
@@ -8,6 +8,7 @@ import (
 	"strings"
 	"time"
 
+	"github.com/skupperproject/skupper/pkg/cleanhttp"
 	"github.com/skupperproject/skupper/pkg/utils"
 	"github.com/spf13/cobra"
 )
@@ -15,18 +16,17 @@ import (
 func get(path string, output string) error {
 	var resp *http.Response
 	var err error
-
+	client := cleanhttp.DefaultClient()
+	client.Timeout = time.Second * 10
 	err = utils.Retry(time.Second, 30, func() (bool, error) {
 		url := "http://localhost:8181/" + path
 		if output == "json" {
 			url += "?output=json"
 		}
-		resp, err = http.Get(url)
+
+		resp, err = client.Get(url)
 		if err != nil {
-			if strings.Contains(err.Error(), "connect: connection refused") {
-				return false, nil
-			}
-			return true, err
+			return false, nil
 		}
 		return true, nil
 	})
diff --git a/cmd/service-controller/main.go b/cmd/service-controller/main.go
index 5a06712d..c2a3769e 100644
--- a/cmd/service-controller/main.go
+++ b/cmd/service-controller/main.go
@@ -77,7 +77,7 @@ func main() {
 	}
 
 	log.Println("Waiting for Skupper router component to start")
-	_, err = kube.WaitDeploymentReady(types.TransportDeploymentName, namespace, cli.KubeClient, time.Second*180, time.Second*5)
+	_, err = kube.WaitDeploymentReady(types.TransportDeploymentName, namespace, cli.KubeClient, time.Second*180, time.Second)
 	if err != nil {
 		log.Fatal("Error waiting for transport deployment to be ready: ", err.Error())
 	}
diff --git a/cmd/skupper/skupper.go b/cmd/skupper/skupper.go
index ee834f17..ad8d3fc7 100644
--- a/cmd/skupper/skupper.go
+++ b/cmd/skupper/skupper.go
@@ -1127,7 +1127,6 @@ func init() {
 	cmdNetwork.AddCommand(NewCmdNetworkStatus(skupperCli.Network()))
 
 	cmdSwitch := NewCmdSwitch()
-	cmdSwitch.Hidden = true
 
 	addCommands(skupperCli, rootCmd,
 		cmdInit,
diff --git a/cmd/skupper/skupper_kube_network.go b/cmd/skupper/skupper_kube_network.go
index 90374d67..1fd68938 100644
--- a/cmd/skupper/skupper_kube_network.go
+++ b/cmd/skupper/skupper_kube_network.go
@@ -63,19 +63,25 @@ func (s *SkupperKubeNetwork) Status(cmd *cobra.Command, args []string) error {
 		for _, siteStatus := range sitesStatus {
 			if len(siteNetworkStatus) == 0 || siteNetworkStatus == siteStatus.Site.Name {
 
-				siteVersion := siteStatus.Site.Version
+				siteVersion := "-"
+				if len(siteStatus.Site.Version) > 0 {
+					siteVersion = siteStatus.Site.Version
+				}
+
 				if len(siteStatus.Site.MinimumVersion) > 0 {
 					siteVersion = fmt.Sprintf("%s (minimum version required %s)", siteStatus.Site.Version, siteStatus.Site.MinimumVersion)
 				}
 
 				detailsMap := map[string]string{"site name": siteStatus.Site.Name, "namespace": siteStatus.Site.Namespace, "version": siteVersion}
 
-				location := "remote"
+				location := "[remote]"
 				if siteStatus.Site.Identity == currentSite {
-					location = "local"
+					location = "[local]"
+				} else if strings.HasPrefix(siteStatus.Site.Identity, "gateway") {
+					location = ""
 				}
 
-				newItem := fmt.Sprintf("[%s] %s(%s) ", location, siteStatus.Site.Identity, siteStatus.Site.Namespace)
+				newItem := fmt.Sprintf("%s %s(%s) ", location, siteStatus.Site.Identity, siteStatus.Site.Namespace)
 				newItem = newItem + fmt.Sprintln()
 
 				siteLevel := networkList.NewChildWithDetail(newItem, detailsMap)
@@ -102,7 +108,7 @@ func (s *SkupperKubeNetwork) Status(cmd *cobra.Command, args []string) error {
 							routerId := strings.Split(routerStatus.Router.Name, "/")
 
 							// skip routers that belong to headless services
-							if len(routerId) > 1 && strings.HasPrefix(routerId[1], siteStatus.Site.Name) {
+							if network.PrintableRouter(routerStatus, siteStatus.Site.Name) {
 								routerItem := fmt.Sprintf("name: %s\n", routerId[1])
 								detailsRouter := map[string]string{"image name": routerStatus.Router.ImageName, "image version": routerStatus.Router.ImageVersion}
 
diff --git a/cmd/skupper/skupper_kube_service.go b/cmd/skupper/skupper_kube_service.go
index 4d37bb4b..f75ac4a6 100644
--- a/cmd/skupper/skupper_kube_service.go
+++ b/cmd/skupper/skupper_kube_service.go
@@ -94,7 +94,11 @@ func (s *SkupperKubeService) Status(cmd *cobra.Command, args []string) error {
 				if mapServiceSites[si.Name] != nil {
 					for _, site := range mapServiceSites[si.Name] {
 						item := site.Site.Identity + "(" + site.Site.Namespace + ")\n"
-						theSite := sites.NewChildWithDetail(item, map[string]string{"policy": site.Site.Policy})
+						policy := "-"
+						if len(site.Site.Policy) > 0 {
+							policy = site.Site.Policy
+						}
+						theSite := sites.NewChildWithDetail(item, map[string]string{"policy": policy})
 
 						if si.ConnectorCount > 0 {
 							t := mapSiteTarget[site.Site.Identity][si.Name]
diff --git a/cmd/skupper/skupper_kube_site.go b/cmd/skupper/skupper_kube_site.go
index 2c73e195..8d888093 100644
--- a/cmd/skupper/skupper_kube_site.go
+++ b/cmd/skupper/skupper_kube_site.go
@@ -341,7 +341,7 @@ func (s *SkupperKubeSite) Update(cmd *cobra.Command, args []string) error {
 		return err
 	}
 	if updated {
-		fmt.Println("Skupper is now updated in '" + cli.GetNamespace() + "'.")
+		fmt.Println("Skupper update in progress for '" + cli.GetNamespace() + "'.")
 	} else {
 		fmt.Println("No update required in '" + cli.GetNamespace() + "'.")
 	}
diff --git a/cmd/skupper/skupper_podman_site.go b/cmd/skupper/skupper_podman_site.go
index d4730e5a..d89ab2bd 100644
--- a/cmd/skupper/skupper_podman_site.go
+++ b/cmd/skupper/skupper_podman_site.go
@@ -1,10 +1,12 @@
 package main
 
 import (
+	"context"
 	"fmt"
 	"net"
 	"os"
 	"strings"
+	"time"
 
 	"github.com/google/uuid"
 	"github.com/skupperproject/skupper/api/types"
@@ -32,6 +34,7 @@ type PodmanInitFlags struct {
 	ContainerNetwork             string
 	EnableIPV6                   bool
 	PodmanEndpoint               string
+	Timeout                      time.Duration
 }
 
 func (s *SkupperPodmanSite) Create(cmd *cobra.Command, args []string) error {
@@ -127,7 +130,9 @@ func (s *SkupperPodmanSite) Create(cmd *cobra.Command, args []string) error {
 
 	// Initializing
 	cmd.SilenceUsage = true
-	err = siteHandler.Create(site)
+	ctx, cn := context.WithTimeout(context.Background(), s.flags.Timeout)
+	defer cn()
+	err = siteHandler.Create(ctx, site)
 	if err != nil {
 		return fmt.Errorf("Error initializing Skupper - %w", err)
 	}
@@ -180,6 +185,8 @@ func (s *SkupperPodmanSite) CreateFlags(cmd *cobra.Command) {
 		"ingress host binding port used for flow-collector and console")
 	cmd.Flags().DurationVar(&routerCreateOpts.FlowCollector.FlowRecordTtl, "flow-collector-record-ttl", 0, "Time after which terminated flow records are deleted, i.e. those flow records that have an end time set. Default is 30 minutes.")
 
+	cmd.Flags().DurationVar(&s.flags.Timeout, "timeout", types.DefaultTimeoutDuration, "Configurable timeout for site initialization")
+
 }
 
 func (s *SkupperPodmanSite) Delete(cmd *cobra.Command, args []string) error {
@@ -292,13 +299,16 @@ func (s *SkupperPodmanSite) Update(cmd *cobra.Command, args []string) error {
 	// updates site version number (always the last one)
 	s.up.RegisterTasks(update.NewVersionUpdateTask(s.podman.cli))
 	// process eligible tasks
-	return s.up.Process(s.podman.currentSite.Version)
+	ctx, cn := context.WithTimeout(context.Background(), s.up.Timeout)
+	defer cn()
+	return s.up.Process(ctx, s.podman.currentSite.Version)
 }
 
 func (s *SkupperPodmanSite) UpdateFlags(cmd *cobra.Command) {
 	s.up = &domain.UpdateProcessor{}
 	cmd.Flags().BoolVar(&s.up.DryRun, "dry-run", false, "only prints the tasks to be performed, but does not run any action")
 	cmd.Flags().BoolVar(&s.up.Verbose, "verbose", false, "displays tasks and post tasks being executed")
+	cmd.Flags().DurationVar(&s.up.Timeout, "timeout", types.DefaultTimeoutDuration, "Configurable timeout for site update")
 }
 
 func (s *SkupperPodmanSite) Version(cmd *cobra.Command, args []string) error {
diff --git a/pkg/cleanhttp/cleanhttp.go b/pkg/cleanhttp/cleanhttp.go
new file mode 100644
index 00000000..90b29d35
--- /dev/null
+++ b/pkg/cleanhttp/cleanhttp.go
@@ -0,0 +1,37 @@
+// cleanhttp has convenience functions for creating clean http clients and
+// transports - free of the globally mutable state in http.DefaultClient and
+// http.DefaultTransport variables.
+package cleanhttp
+
+import (
+	"net"
+	"net/http"
+	"time"
+)
+
+// DefaultClient returns a clean http Client using the same defaults as go's
+// http.DefaultClient without relying on global state shared with other
+// clients.
+func DefaultClient() *http.Client {
+	return &http.Client{
+		Transport: DefaultTransport(),
+	}
+}
+
+// DefaultTransport returns a clean http Transport with the same defaults as
+// go's http.DefaultTransport.
+func DefaultTransport() *http.Transport {
+	transport := &http.Transport{
+		Proxy: http.ProxyFromEnvironment,
+		DialContext: (&net.Dialer{
+			Timeout:   30 * time.Second,
+			KeepAlive: 30 * time.Second,
+		}).DialContext,
+		ForceAttemptHTTP2:     true,
+		MaxIdleConns:          100,
+		IdleConnTimeout:       90 * time.Second,
+		TLSHandshakeTimeout:   10 * time.Second,
+		ExpectContinueTimeout: 1 * time.Second,
+	}
+	return transport
+}
diff --git a/pkg/domain/podman/deployment.go b/pkg/domain/podman/deployment.go
index a8d26b87..2048d5d0 100644
--- a/pkg/domain/podman/deployment.go
+++ b/pkg/domain/podman/deployment.go
@@ -1,6 +1,7 @@
 package podman
 
 import (
+	"context"
 	"fmt"
 	"strconv"
 	"strings"
@@ -35,7 +36,7 @@ func NewSkupperDeploymentHandlerPodman(cli *podman.PodmanRestClient) *SkupperDep
 }
 
 // Deploy deploys each component as a container
-func (s *SkupperDeploymentHandler) Deploy(deployment domain.SkupperDeployment) error {
+func (s *SkupperDeploymentHandler) Deploy(ctx context.Context, deployment domain.SkupperDeployment) error {
 	var err error
 	var cleanupContainers []string
 
@@ -56,7 +57,7 @@ func (s *SkupperDeploymentHandler) Deploy(deployment domain.SkupperDeployment) e
 	for _, component := range deployment.GetComponents() {
 
 		// Pulling image first
-		err = s.cli.ImagePull(component.GetImage())
+		err = s.cli.ImagePull(ctx, component.GetImage())
 		if err != nil {
 			return err
 		}
diff --git a/pkg/domain/podman/site.go b/pkg/domain/podman/site.go
index e4e04faf..a2aa142a 100644
--- a/pkg/domain/podman/site.go
+++ b/pkg/domain/podman/site.go
@@ -1,6 +1,7 @@
 package podman
 
 import (
+	"context"
 	"fmt"
 	"os"
 	"path"
@@ -98,7 +99,7 @@ func NewSitePodmanHandler(endpoint string) (*SiteHandler, error) {
 	}, nil
 }
 
-func (s *SiteHandler) prepare(site domain.Site) (domain.Site, error) {
+func (s *SiteHandler) prepare(ctx context.Context, site domain.Site) (domain.Site, error) {
 	podmanSite, ok := site.(*Site)
 
 	if !ok {
@@ -125,7 +126,7 @@ func (s *SiteHandler) prepare(site domain.Site) (domain.Site, error) {
 	domain.ConfigureSiteCredentials(podmanSite, podmanSite.IngressHosts...)
 	s.ConfigurePodmanDeployments(podmanSite)
 
-	if err := s.canCreate(podmanSite); err != nil {
+	if err := s.canCreate(ctx, podmanSite); err != nil {
 		return nil, err
 	}
 
@@ -209,13 +210,13 @@ func (s *SiteHandler) prepareRouterDeployment(site *Site) *SkupperDeployment {
 	return routerDepl
 }
 
-func (s *SiteHandler) Create(site domain.Site) error {
+func (s *SiteHandler) Create(ctx context.Context, site domain.Site) error {
 	var err error
 	var cleanupFns []func()
 
 	var preparedSite domain.Site
 	podmanSite := site.(*Site)
-	preparedSite, err = s.prepare(podmanSite)
+	preparedSite, err = s.prepare(ctx, podmanSite)
 	if err != nil {
 		return err
 	}
@@ -320,7 +321,7 @@ func (s *SiteHandler) Create(site domain.Site) error {
 	// Deploy container(s)
 	deployHandler := NewSkupperDeploymentHandlerPodman(s.cli)
 	for _, depl := range podmanSite.GetDeployments() {
-		err = deployHandler.Deploy(depl)
+		err = deployHandler.Deploy(ctx, depl)
 		if err != nil {
 			return err
 		}
@@ -351,7 +352,7 @@ func (s *SiteHandler) Create(site domain.Site) error {
 	return nil
 }
 
-func (s *SiteHandler) canCreate(site *Site) error {
+func (s *SiteHandler) canCreate(ctx context.Context, site *Site) error {
 
 	// Validating podman endpoint
 	if s.cli == nil {
@@ -439,7 +440,7 @@ func (s *SiteHandler) canCreate(site *Site) error {
 	}
 
 	// Validating router container runs without errors
-	err = s.runTempContainer()
+	err = s.runTempContainer(ctx)
 	if err != nil {
 		return fmt.Errorf("site cannot be created: %s", err)
 	}
@@ -978,9 +979,9 @@ func (s *SiteHandler) createPrometheusConfigFiles(site *Site) error {
 	return nil
 }
 
-func (s *SiteHandler) runTempContainer() error {
+func (s *SiteHandler) runTempContainer(ctx context.Context) error {
 	cli := s.cli
-	err := cli.ImagePull(images.GetRouterImageName())
+	err := cli.ImagePull(ctx, images.GetRouterImageName())
 	if err != nil {
 		return err
 	}
diff --git a/pkg/domain/podman/update/common.go b/pkg/domain/podman/update/common.go
index ced175d6..d44ee4e0 100644
--- a/pkg/domain/podman/update/common.go
+++ b/pkg/domain/podman/update/common.go
@@ -1,6 +1,7 @@
 package update
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/skupperproject/skupper/api/types"
@@ -41,7 +42,7 @@ func (v *VersionUpdateTask) Priority() domain.UpdatePriority {
 	return domain.PriorityLast
 }
 
-func (v *VersionUpdateTask) Run() *domain.UpdateResult {
+func (v *VersionUpdateTask) Run(context.Context) *domain.UpdateResult {
 	var res = &domain.UpdateResult{}
 	ch := podman.NewRouterConfigHandlerPodman(v.cli)
 	cfg, err := ch.GetRouterConfig()
@@ -89,7 +90,7 @@ func (u *ContainerImagesTask) Priority() domain.UpdatePriority {
 	return domain.PriorityNormal
 }
 
-func (u *ContainerImagesTask) Run() *domain.UpdateResult {
+func (u *ContainerImagesTask) Run(ctx context.Context) *domain.UpdateResult {
 	var result = &domain.UpdateResult{}
 	sh := podman.NewSitePodmanHandlerFromCli(u.cli)
 	site, err := sh.Get()
@@ -112,7 +113,7 @@ func (u *ContainerImagesTask) Run() *domain.UpdateResult {
 				image = images.GetPrometheusServerImageName()
 			}
 			if image != cmp.GetImage() {
-				_, err = u.cli.ContainerUpdateImage(cmp.Name(), image)
+				_, err = u.cli.ContainerUpdateImage(ctx, cmp.Name(), image)
 				if err != nil {
 					result.AddErrors(fmt.Errorf("error updating container: %s - image: %s - %s",
 						cmp.Name(), image, err))
@@ -123,7 +124,7 @@ func (u *ContainerImagesTask) Run() *domain.UpdateResult {
 		}
 	}
 	// updating service containers
-	updSvcResult := u.updateServiceContainers()
+	updSvcResult := u.updateServiceContainers(ctx)
 	if len(updSvcResult.Errors) > 0 {
 		result.AddErrors(updSvcResult.Errors...)
 		return result
@@ -134,7 +135,7 @@ func (u *ContainerImagesTask) Run() *domain.UpdateResult {
 	return result
 }
 
-func (u *ContainerImagesTask) updateServiceContainers() domain.UpdateResult {
+func (u *ContainerImagesTask) updateServiceContainers(ctx context.Context) domain.UpdateResult {
 	var result domain.UpdateResult
 	sh := podman.NewServiceHandlerPodman(u.cli)
 	services, err := sh.List()
@@ -151,7 +152,7 @@ func (u *ContainerImagesTask) updateServiceContainers() domain.UpdateResult {
 			return result
 		}
 		if c.Image != images.GetRouterImageName() {
-			_, err = u.cli.ContainerUpdateImage(svcPodman.ContainerName, images.GetRouterImageName())
+			_, err = u.cli.ContainerUpdateImage(ctx, svcPodman.ContainerName, images.GetRouterImageName())
 			if err != nil {
 				result.AddErrors(fmt.Errorf("error updating service container image for %s: %s",
 					svcPodman.ContainerName, err))
diff --git a/pkg/domain/site.go b/pkg/domain/site.go
index 11539fc0..f61f1222 100644
--- a/pkg/domain/site.go
+++ b/pkg/domain/site.go
@@ -1,6 +1,7 @@
 package domain
 
 import (
+	"context"
 	"fmt"
 
 	"github.com/google/uuid"
@@ -27,7 +28,7 @@ type Site interface {
 }
 
 type SiteHandler interface {
-	Create(s Site) error
+	Create(ctx context.Context, s Site) error
 	Get() (Site, error)
 	Delete() error
 	Update() error
diff --git a/pkg/domain/skupper_deployment.go b/pkg/domain/skupper_deployment.go
index 8ec6c5e9..1730f174 100644
--- a/pkg/domain/skupper_deployment.go
+++ b/pkg/domain/skupper_deployment.go
@@ -1,12 +1,14 @@
 package domain
 
+import "context"
+
 type SkupperDeployment interface {
 	GetName() string
 	GetComponents() []SkupperComponent
 }
 
 type SkupperDeploymentHandler interface {
-	Deploy(deployment SkupperDeployment) error
+	Deploy(ctx context.Context, deployment SkupperDeployment) error
 	List() ([]SkupperDeployment, error)
 	Undeploy(name string) error
 }
diff --git a/pkg/domain/update.go b/pkg/domain/update.go
index d65974c6..2c421bd1 100644
--- a/pkg/domain/update.go
+++ b/pkg/domain/update.go
@@ -1,9 +1,11 @@
 package domain
 
 import (
+	"context"
 	"fmt"
 	"reflect"
 	"sort"
+	"time"
 
 	"github.com/skupperproject/skupper/pkg/utils"
 )
@@ -60,7 +62,7 @@ type UpdateTask interface {
 	// Priority determines how tasks will be sorted by the UpdateProcessor
 	Priority() UpdatePriority
 	// Run method is where the update task is done
-	Run() *UpdateResult
+	Run(ctx context.Context) *UpdateResult
 }
 
 type UpdateProcessor struct {
@@ -69,6 +71,7 @@ type UpdateProcessor struct {
 	changes   []string
 	Verbose   bool
 	DryRun    bool
+	Timeout   time.Duration
 }
 
 func (p *UpdateProcessor) Println(a ...any) {
@@ -106,14 +109,14 @@ func (p *UpdateProcessor) registerPostTasks(tasks ...UpdateTask) {
 	}
 }
 
-func (p *UpdateProcessor) Process(siteVersion string) error {
-	err := p.process(siteVersion, p.tasks)
+func (p *UpdateProcessor) Process(ctx context.Context, siteVersion string) error {
+	err := p.process(ctx, siteVersion, p.tasks)
 	if err != nil {
 		return err
 	}
 	if len(p.postTasks) > 0 {
 		p.Println("Post update tasks")
-		return p.process(siteVersion, p.postTasks)
+		return p.process(ctx, siteVersion, p.postTasks)
 	}
 	if len(p.changes) > 0 {
 		fmt.Println("Skupper is now updated for '" + utils.ReadUsername() + "'.")
@@ -123,7 +126,7 @@ func (p *UpdateProcessor) Process(siteVersion string) error {
 	return nil
 }
 
-func (p *UpdateProcessor) process(siteVersion string, tasks []UpdateTask) error {
+func (p *UpdateProcessor) process(ctx context.Context, siteVersion string, tasks []UpdateTask) error {
 	var validTasks []UpdateTask
 	title := "Task"
 	postTasks := false
@@ -170,7 +173,7 @@ func (p *UpdateProcessor) process(siteVersion string, tasks []UpdateTask) error
 			title, i+1, totalTasks, version, task.Priority(), task.Info())
 		var result *UpdateResult
 		if !p.DryRun {
-			result = task.Run()
+			result = task.Run(ctx)
 			if result == nil {
 				result = &UpdateResult{}
 			}
diff --git a/pkg/flow/collector.go b/pkg/flow/collector.go
index 436c196c..ce6b1c54 100644
--- a/pkg/flow/collector.go
+++ b/pkg/flow/collector.go
@@ -3,6 +3,7 @@ package flow
 import (
 	"bytes"
 	"encoding/gob"
+	"fmt"
 	"log"
 	"net/http"
 	"time"
@@ -197,6 +198,9 @@ type FlowCollector struct {
 	connectorsToReconcile   map[string]string
 	processesToReconcile    map[string]*ProcessRecord
 	aggregatesToReconcile   map[string]*FlowPairRecord
+
+	begin           time.Time
+	networkStatusUp bool
 }
 
 func getTtl(ttl time.Duration) time.Duration {
@@ -332,7 +336,7 @@ func (c *FlowCollector) beaconUpdate(beacon BeaconRecord) {
 }
 
 func (c *FlowCollector) recordUpdates(stopCh <-chan struct{}) {
-	tickerFlush := time.NewTicker(1 * time.Second)
+	tickerFlush := time.NewTicker(250 * time.Millisecond)
 	defer tickerFlush.Stop()
 	tickerReconcile := time.NewTicker(2 * time.Second)
 	defer tickerReconcile.Stop()
@@ -398,9 +402,39 @@ func (c *FlowCollector) recordUpdates(stopCh <-chan struct{}) {
 }
 
 func (c *FlowCollector) Start(stopCh <-chan struct{}) {
+	c.begin = time.Now()
 	go c.run(stopCh)
 }
 
+// PrimeSiteBeacons "sends" the collector phony beacon records for a controller
+// and a router event source when their addresses are known in order to avoid
+// the startup time waiting for beacon records.
+func (c *FlowCollector) PrimeSiteBeacons(controllerID, routerID string) {
+	var incoming []interface{}
+	if controllerID != "" {
+		incoming = append(incoming, BeaconRecord{
+			Version:    1,
+			SourceType: "CONTROLLER",
+			Address:    fmt.Sprintf("mc/sfe.%s", controllerID),
+			Direct:     fmt.Sprintf("sfe.%s", controllerID),
+			Identity:   controllerID,
+		})
+	}
+	if routerID != "" {
+		incoming = append(incoming, BeaconRecord{
+			Version:    1,
+			SourceType: "ROUTER",
+			Address:    fmt.Sprintf("mc/sfe.%s", routerID),
+			Direct:     fmt.Sprintf("sfe.%s", routerID),
+			Identity:   routerID,
+		})
+	}
+	if len(incoming) == 0 {
+		return
+	}
+	c.beaconsIncoming <- incoming
+}
+
 func (c *FlowCollector) run(stopCh <-chan struct{}) {
 	if c.mode == RecordMetrics {
 		c.metrics = c.NewMetrics(c.prometheusReg)
diff --git a/pkg/flow/controller.go b/pkg/flow/controller.go
index 989647b2..885097b6 100644
--- a/pkg/flow/controller.go
+++ b/pkg/flow/controller.go
@@ -87,19 +87,21 @@ func UpdateProcess(c *FlowController, deleted bool, name string, process *Proces
 }
 
 func UpdateHost(c *FlowController, deleted bool, name string, host *HostRecord) error {
-	if !deleted && host != nil {
+	if deleted {
+		for id, host := range c.hostRecords {
+			if *host.Name == name {
+				host.EndTime = uint64(time.Now().UnixNano()) / uint64(time.Microsecond)
+				delete(c.hostRecords, id)
+				break
+			}
+		}
+	} else if host != nil {
 		host.RecType = recordNames[Host]
 		if _, ok := c.hostRecords[host.Identity]; !ok {
 			c.hostRecords[host.Identity] = host
 		}
 		c.hostOutgoing <- host
-	} else {
-		if existing, ok := c.hostRecords[host.Identity]; ok {
-			existing.EndTime = uint64(time.Now().UnixNano()) / uint64(time.Microsecond)
-		}
-		delete(c.hostRecords, host.Identity)
 	}
-
 	return nil
 }
 
diff --git a/pkg/flow/flow_mem_driver.go b/pkg/flow/flow_mem_driver.go
index 4cee897b..b5b3b7f2 100644
--- a/pkg/flow/flow_mem_driver.go
+++ b/pkg/flow/flow_mem_driver.go
@@ -487,6 +487,8 @@ var defaultRetry = wait.Backoff{
 	Jitter:   0.1,
 }
 
+var netUpdateCt int
+
 func (fc *FlowCollector) updateNetworkStatus() error {
 	var err error
 	networkData := map[string]string{}
@@ -545,9 +547,14 @@ func (fc *FlowCollector) updateNetworkStatus() error {
 			if err != nil {
 				return err
 			} else {
+				netUpdateCt++
 				return nil
 			}
 		})
+		if !fc.networkStatusUp && len(networkStatus.Sites) > 0 && len(networkStatus.Sites[0].RouterStatus) > 0 {
+			fc.networkStatusUp = true
+			log.Printf("COLLECTOR: First functional network status update written after %s and %d updates\n", time.Since(fc.begin), netUpdateCt)
+		}
 	}
 	return err
 }
diff --git a/pkg/flow/record.go b/pkg/flow/record.go
index 5691058d..cb8879ab 100644
--- a/pkg/flow/record.go
+++ b/pkg/flow/record.go
@@ -474,15 +474,15 @@ type Payload struct {
 }
 
 type QueryParams struct {
-	Offset             int               `json:"offset"`
-	Limit              int               `json:"limit"`
-	SortBy             string            `json:"sortBy"`
-	Filter             string            `json:"filter"`
-	FilterFields       map[string]string `json:"filterFields"`
-	TimeRangeStart     uint64            `json:"timeRangeStart"`
-	TimeRangeEnd       uint64            `json:"timeRangeEnd"`
-	TimeRangeOperation TimeRangeRelation `json:"timeRangeOperation"`
-	State              RecordState       `json:"state"`
+	Offset             int                 `json:"offset"`
+	Limit              int                 `json:"limit"`
+	SortBy             string              `json:"sortBy"`
+	Filter             string              `json:"filter"`
+	FilterFields       map[string][]string `json:"filterFields"`
+	TimeRangeStart     uint64              `json:"timeRangeStart"`
+	TimeRangeEnd       uint64              `json:"timeRangeEnd"`
+	TimeRangeOperation TimeRangeRelation   `json:"timeRangeOperation"`
+	State              RecordState         `json:"state"`
 }
 
 func getQueryParams(url *url.URL) QueryParams {
@@ -492,7 +492,7 @@ func getQueryParams(url *url.URL) QueryParams {
 		Limit:              -1,
 		SortBy:             "identity.asc",
 		Filter:             "",
-		FilterFields:       make(map[string]string),
+		FilterFields:       make(map[string][]string),
 		TimeRangeStart:     now - (15 * oneMinute),
 		TimeRangeEnd:       now,
 		TimeRangeOperation: intersects,
@@ -556,7 +556,7 @@ func getQueryParams(url *url.URL) QueryParams {
 				qp.State = all
 			}
 		default:
-			qp.FilterFields[cases.Title(language.Und, cases.NoLower).String(k)] = v[0]
+			qp.FilterFields[cases.Title(language.Und, cases.NoLower).String(k)] = v
 		}
 	}
 	return qp
@@ -700,33 +700,37 @@ func getField(field string, record interface{}) interface{} {
 	return nil
 }
 
-func matchFieldValue(x interface{}, y string) bool {
-	if x != nil && y != "" {
-		switch x.(type) {
-		case string:
-			return strings.HasPrefix(x.(string), y)
-		case uint64:
-			i, err := strconv.ParseInt(y, 10, 64)
-			if err == nil {
-				return x.(uint64) == uint64(i)
-			}
-		case int32:
-			i, err := strconv.ParseInt(y, 10, 32)
-			if err == nil {
-				return x.(int32) == int32(i)
-			}
-		case int64:
-			i, err := strconv.ParseInt(y, 10, 64)
-			if err == nil {
-				return x.(int64) == int64(i)
+func matchFieldValues(x interface{}, values []string) bool {
+	if x == nil || len(values) == 0 {
+		return false
+	}
+	switch x := x.(type) {
+	case string:
+		for _, y := range values {
+			if y == "" {
+				continue
 			}
-		case int:
-			i, err := strconv.ParseInt(y, 10, 64)
-			if err == nil {
-				return x.(int) == int(i)
+			if strings.HasPrefix(x, y) {
+				return true
 			}
-		default:
-			return false
+		}
+	case uint64:
+		return numInStringSlice(x, values)
+	case int32:
+		return numInStringSlice(x, values)
+	case int64:
+		return numInStringSlice(x, values)
+	case int:
+		return numInStringSlice(x, values)
+	}
+	return false
+}
+
+func numInStringSlice[T int | uint64 | int64 | int32](x T, values []string) bool {
+	for _, value := range values {
+		i, err := strconv.ParseInt(value, 10, 64)
+		if err == nil && x == T(i) {
+			return true
 		}
 	}
 	return false
@@ -773,9 +777,9 @@ func compareFields(x, y interface{}, order string) bool {
 	}
 }
 
-// type any = interface{}
-func filterRecord[T any](item T, qp QueryParams) bool {
+func filterRecord(item any, qp QueryParams) bool {
 	filter := true
+	//TODO(ck) deprecate filter?
 	if qp.Filter != "" {
 		field, match, err := validateAndReturnFilterQuery(qp.Filter)
 		// todo propagate error or log
@@ -791,7 +795,7 @@ func filterRecord[T any](item T, qp QueryParams) bool {
 				if !filterSubRecord(value, match) {
 					filter = false
 				}
-			} else if !matchFieldValue(value, match) {
+			} else if !matchFieldValues(value, []string{match}) {
 				filter = false
 			}
 		}
@@ -807,10 +811,10 @@ func filterRecord[T any](item T, qp QueryParams) bool {
 			} else {
 				x := reflect.ValueOf(value)
 				if x.Kind() == reflect.Struct {
-					if !filterSubRecord(value, subField+"."+match) {
+					if !filterSubRecordValues(value, subField, match) {
 						filter = false
 					}
-				} else if !matchFieldValue(value, match) {
+				} else if !matchFieldValues(value, match) {
 					filter = false
 				}
 			}
@@ -819,8 +823,22 @@ func filterRecord[T any](item T, qp QueryParams) bool {
 	return filter
 }
 
-// type any = interface{}
-func filterSubRecord[T any](item T, filter string) bool {
+func filterSubRecordValues(item interface{}, spec string, values []string) bool {
+	if spec == "" {
+		return true
+	}
+	titleCaser := cases.Title(language.Und, cases.NoLower)
+	parts := strings.Split(spec, ".")
+	field, spec := titleCaser.String(parts[0]), strings.Join(parts[1:], ".")
+	value := getField(field, item)
+	x := reflect.ValueOf(value)
+	if x.Kind() == reflect.Struct {
+		return filterSubRecordValues(value, spec, values)
+	}
+	return matchFieldValues(value, values)
+}
+
+func filterSubRecord(item interface{}, filter string) bool {
 	if filter == "" {
 		return true
 	}
@@ -834,7 +852,7 @@ func filterSubRecord[T any](item T, filter string) bool {
 	if x.Kind() == reflect.Struct {
 		return filterSubRecord(value, match)
 	}
-	return matchFieldValue(value, match)
+	return matchFieldValues(value, []string{match})
 }
 
 func sortAndSlice[T any](list []T, payload *Payload, queryParams QueryParams) error {
diff --git a/pkg/kube/deployments.go b/pkg/kube/deployments.go
index c0380c69..2f366102 100644
--- a/pkg/kube/deployments.go
+++ b/pkg/kube/deployments.go
@@ -18,17 +18,6 @@ import (
 	"github.com/skupperproject/skupper/api/types"
 )
 
-func GetDeploymentLabel(name string, key string, namespace string, cli kubernetes.Interface) string {
-	deployment, err := GetDeployment(name, namespace, cli)
-	if err != nil {
-		return ""
-	}
-	if value, ok := deployment.Spec.Template.Labels[key]; ok {
-		return value
-	}
-	return ""
-}
-
 func GetPods(selector string, namespace string, cli kubernetes.Interface) ([]corev1.Pod, error) {
 	options := metav1.ListOptions{LabelSelector: selector}
 	podList, err := cli.CoreV1().Pods(namespace).List(context.TODO(), options)
diff --git a/pkg/network/network.go b/pkg/network/network.go
index 4fbd5871..501e0eb5 100644
--- a/pkg/network/network.go
+++ b/pkg/network/network.go
@@ -123,10 +123,7 @@ func (s *SkupperStatus) LinkBelongsToSameSite(linkName string, siteId string, ro
 func (s *SkupperStatus) GetRouterIndex(site *SiteStatusInfo) (error, int) {
 
 	for index, router := range site.RouterStatus {
-		// Ignore routers that belong to statefulsets for headless services and any other router
-		routerId := strings.Split(router.Router.Name, "/")
-
-		if len(routerId) > 1 && strings.HasPrefix(routerId[1], site.Site.Name) {
+		if PrintableRouter(router, site.Site.Name) {
 			return nil, index
 
 		}
@@ -161,6 +158,16 @@ func UnmarshalSkupperStatus(data map[string]string) (*NetworkStatusInfo, error)
 	return networkStatusInfo, nil
 }
 
+func PrintableRouter(router RouterStatusInfo, siteName string) bool {
+	// Ignore routers that belong to statefulsets for headless services and any other router
+	routerId := strings.Split(router.Router.Name, "/")
+
+	isARegularSite := len(routerId) > 1 && strings.HasPrefix(routerId[1], siteName)
+	isAGateway := len(routerId) > 1 && strings.HasPrefix(routerId[1], "skupper-gateway")
+
+	return isARegularSite || isAGateway
+}
+
 func sliceContainsSite(sites []SiteStatusInfo, site SiteStatusInfo) bool {
 	for _, s := range sites {
 		if site.Site.Identity == s.Site.Identity {


Test patch:
diff --git a/client/podman/container_test.go b/client/podman/container_test.go
index 14c077ae..e7829845 100644
--- a/client/podman/container_test.go
+++ b/client/podman/container_test.go
@@ -1,9 +1,11 @@
 package podman
 
 import (
+	"bytes"
 	"context"
 	"fmt"
 	"os"
+	"os/exec"
 	"path"
 	"strings"
 	"sync"
@@ -113,7 +115,18 @@ func TestContainer(t *testing.T) {
 
 	// Pulling image
 	t.Run("image-pull", func(t *testing.T) {
-		assert.Assert(t, cli.ImagePull(image))
+		assert.Assert(t, cli.ImagePull(ctx, image))
+		invalidImage := strings.Replace(images.GetSiteControllerImageName(), ":main", ":invalid", 1)
+		invalidImageErr := cli.ImagePull(ctx, invalidImage)
+		assert.Assert(t, invalidImageErr != nil)
+		assert.Assert(t, strings.Contains(invalidImageErr.Error(), "Recommendation:"))
+	})
+	t.Run("image-pull-timeout", func(t *testing.T) {
+		expCtx, expCn := context.WithTimeout(context.Background(), time.Millisecond)
+		time.Sleep(time.Millisecond)
+		defer expCn()
+		expErr := cli.ImagePull(expCtx, image)
+		assert.ErrorContains(t, expErr, "context deadline exceeded")
 	})
 
 	// Creating container
@@ -168,9 +181,15 @@ func TestContainer(t *testing.T) {
 	t.Run("container-inspect", containerInspectTest)
 
 	t.Run("container-exec", func(t *testing.T) {
-		out, err := cli.ContainerExec(name, strings.Split("ls -1 /app", " "))
+		out, err := cli.ContainerExec(name, strings.Split("cat /etc/services", " "))
 		assert.Assert(t, err)
-		assert.Assert(t, strings.Contains(out, "service-controller"))
+		assert.Assert(t, len(out) > 1)
+		cmd := exec.CommandContext(ctx, "podman", strings.Split(fmt.Sprintf("exec %s cat /etc/services", name), " ")...)
+		stdout := &bytes.Buffer{}
+		cmd.Stdout = stdout
+		assert.Assert(t, cmd.Run())
+		assert.Assert(t, len(stdout.String()) > 1)
+		assert.Equal(t, out, stdout.String())
 	})
 
 	// Disconnecting container from network
@@ -193,13 +212,38 @@ func TestContainer(t *testing.T) {
 	// Updating container image
 	image = strings.ReplaceAll(image, ":main", ":latest")
 	t.Run("container-update-image", func(t *testing.T) {
-		c, err := cli.ContainerUpdateImage(name, image)
+		c, err := cli.ContainerUpdateImage(ctx, name, image)
 		assert.Assert(t, err)
 		assert.Equal(t, c.Image, image)
 		containerInspectTest(t)
 	})
 	t.Run("container-inspect-after-image-update", containerInspectTest)
 
+	t.Run("container-logs", func(t *testing.T) {
+		clogsName := RandomName("skupper-test")
+		assert.Assert(t, cli.ImagePull(ctx, images.GetRouterImageName()))
+		err = cli.ContainerCreate(&container.Container{
+			Name:        clogsName,
+			Image:       images.GetRouterImageName(),
+			Env:         env,
+			Labels:      labels,
+			Annotations: annotations,
+		})
+		assert.Assert(t, cli.ContainerStart(clogsName))
+		time.Sleep(time.Second * 5)
+		assert.Assert(t, cli.ContainerStop(clogsName))
+		logs, err := cli.ContainerLogs(clogsName)
+		assert.Assert(t, err)
+		assert.Assert(t, len(logs) > 1)
+		cmd := exec.CommandContext(ctx, "podman", strings.Split(fmt.Sprintf("logs %s", clogsName), " ")...)
+		stderr := &bytes.Buffer{}
+		cmd.Stderr = stderr
+		assert.Assert(t, cmd.Run())
+		assert.Assert(t, len(stderr.String()) > 1)
+		assert.Equal(t, logs, stderr.String())
+		assert.Assert(t, cli.ContainerRemove(clogsName))
+	})
+
 	// Cleaning up
 	t.Run("cleanup", func(t *testing.T) {
 		var cleanupErrors []error
@@ -258,7 +302,7 @@ func TestContainerUpdateMock(t *testing.T) {
 	startedAt := cc.StartedAt
 
 	newImage := strings.Replace(image, ":main", ":updated", -1)
-	_, err = cli.ContainerUpdateImage("my-container", newImage)
+	_, err = cli.ContainerUpdateImage(context.Background(), "my-container", newImage)
 	assert.Assert(t, err)
 
 	// validating that the container has the new image and has been restarted
diff --git a/client/podman/informer_test.go b/client/podman/informer_test.go
index 08e57b90..86727499 100644
--- a/client/podman/informer_test.go
+++ b/client/podman/informer_test.go
@@ -67,7 +67,7 @@ func TestContainerInformer(t *testing.T) {
 
 	// Pulling image
 	image := images.GetServiceControllerImageName()
-	assert.Assert(t, cli.ImagePull(image))
+	assert.Assert(t, cli.ImagePull(ctx, image))
 
 	var containersLeft = map[string]bool{}
 	for _, s := range scenarios {
diff --git a/cmd/skupper/skupper_podman_test.go b/cmd/skupper/skupper_podman_test.go
index eeefc078..aa5da321 100644
--- a/cmd/skupper/skupper_podman_test.go
+++ b/cmd/skupper/skupper_podman_test.go
@@ -2,6 +2,7 @@ package main
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"strings"
 	"testing"
@@ -153,7 +154,7 @@ type siteHandlerMock struct {
 	getHook func() (domain.Site, error)
 }
 
-func (sh *siteHandlerMock) Create(s domain.Site) error {
+func (sh *siteHandlerMock) Create(ctx context.Context, s domain.Site) error {
 	return fmt.Errorf("not implemented")
 }
 
diff --git a/pkg/domain/podman/link_test.go b/pkg/domain/podman/link_test.go
index 4ec1be5d..08ead78c 100644
--- a/pkg/domain/podman/link_test.go
+++ b/pkg/domain/podman/link_test.go
@@ -21,7 +21,7 @@ func TestLinkHandlerPodman(t *testing.T) {
 	// Create a basic podman site
 	siteHandler, err := NewSitePodmanHandler(getEndpoint())
 	assert.Assert(t, err)
-	assert.Assert(t, siteHandler.Create(newBasicSite()))
+	assert.Assert(t, siteHandler.Create(context.Background(), newBasicSite()))
 	defer func() {
 		assert.Assert(t, siteHandler.Delete())
 	}()
diff --git a/pkg/domain/podman/main_test.go b/pkg/domain/podman/main_test.go
index 78abfd00..7ada9a35 100644
--- a/pkg/domain/podman/main_test.go
+++ b/pkg/domain/podman/main_test.go
@@ -52,7 +52,7 @@ func createBasicSite() error {
 	if err != nil {
 		return err
 	}
-	return siteHandler.Create(newBasicSite())
+	return siteHandler.Create(context.Background(), newBasicSite())
 }
 
 func teardownBasicSite() error {
@@ -139,7 +139,7 @@ func configureSiteAndCreateRouter(ctx context.Context, cli *client.VanClient, na
 }
 
 func runNginxContainer() error {
-	if err := cli.ImagePull(NGINX_IMAGE); err != nil {
+	if err := cli.ImagePull(context.Background(), NGINX_IMAGE); err != nil {
 		return err
 	}
 	c := container.Container{
diff --git a/pkg/domain/podman/site_test.go b/pkg/domain/podman/site_test.go
index b278a67f..b274ede2 100644
--- a/pkg/domain/podman/site_test.go
+++ b/pkg/domain/podman/site_test.go
@@ -4,6 +4,7 @@
 package podman
 
 import (
+	"context"
 	_ "embed"
 	"strings"
 	"testing"
@@ -79,7 +80,7 @@ func TestSiteHandler(t *testing.T) {
 	for _, scenario := range scenarios {
 		t.Run(scenario.name, func(t *testing.T) {
 			t.Logf("creating site")
-			err = siteHandler.Create(scenario.site)
+			err = siteHandler.Create(context.Background(), scenario.site)
 			assert.Assert(t, err)
 
 			// remove site
@@ -177,7 +178,7 @@ func TestSiteHandlerDeleteBrokenSite(t *testing.T) {
 	}
 
 	// Create a podman site
-	err = siteHandler.Create(site)
+	err = siteHandler.Create(context.Background(), site)
 	assert.Assert(t, err)
 
 	// remove site
diff --git a/pkg/domain/podman/update/common_test.go b/pkg/domain/podman/update/common_test.go
index 0f81e88d..baf50a89 100644
--- a/pkg/domain/podman/update/common_test.go
+++ b/pkg/domain/podman/update/common_test.go
@@ -4,6 +4,7 @@
 package update
 
 import (
+	"context"
 	_ "embed"
 	"flag"
 	"os"
@@ -74,7 +75,7 @@ func TestContainerImagesTask(t *testing.T) {
 			imagesTask.version = test.version
 			t.Logf("New version: %s - current mock site version: %s", test.version, site.GetVersion())
 			assert.Assert(t, imagesTask.AppliesTo(site.GetVersion()) == test.changed)
-			res := imagesTask.Run()
+			res := imagesTask.Run(context.Background())
 			assert.Assert(t, len(res.Errors) == 0)
 			assert.Assert(t, res.Changed() == test.changed)
 			expectedChanges := 4
@@ -151,7 +152,7 @@ func TestVersionUpdateTask(t *testing.T) {
 
 			// mocking task execution
 			if test.changed {
-				res := versionUpdTask.Run()
+				res := versionUpdTask.Run(context.Background())
 				assert.Assert(t, len(res.Errors) == 0)
 				assert.Assert(t, res.Changed() == test.changed)
 				assert.Assert(t, 1 == len(res.GetChanges()))
diff --git a/pkg/domain/update_test.go b/pkg/domain/update_test.go
index 0a7f5dbf..0765dba0 100644
--- a/pkg/domain/update_test.go
+++ b/pkg/domain/update_test.go
@@ -1,6 +1,7 @@
 package domain
 
 import (
+	"context"
 	"fmt"
 	"testing"
 
@@ -406,7 +407,7 @@ func TestUpdateProcessor(t *testing.T) {
 				Verbose: true,
 			}
 			up.RegisterTasks(scenario.tasks...)
-			err := up.Process(scenario.curVersion)
+			err := up.Process(context.Background(), scenario.curVersion)
 			assert.Equal(t, err != nil, scenario.expectError, fmt.Sprintf("error expected == %v but got err == %v", scenario.expectError, err))
 			assert.DeepEqual(t, scenario.expectedChanges, up.changes)
 		})
@@ -447,6 +448,6 @@ func (u *mockUpdateTask) Priority() UpdatePriority {
 	return u.priority
 }
 
-func (u *mockUpdateTask) Run() *UpdateResult {
+func (u *mockUpdateTask) Run(context.Context) *UpdateResult {
 	return u.result
 }
diff --git a/pkg/flow/controller_test.go b/pkg/flow/controller_test.go
index 27adcbaf..f10c66cb 100644
--- a/pkg/flow/controller_test.go
+++ b/pkg/flow/controller_test.go
@@ -55,7 +55,7 @@ func TestUpdateHost(t *testing.T) {
 	hostUpdate := <-fc.hostOutgoing
 	assert.Assert(t, hostUpdate != nil)
 	assert.Equal(t, hostUpdate.Parent, "mysite")
-	err = UpdateHost(fc, true, "bastion-server", host)
+	err = UpdateHost(fc, true, "bastion-server", nil)
 	assert.Assert(t, err)
 	assert.Equal(t, len(fc.hostRecords), 0)
 }
diff --git a/pkg/flow/record_test.go b/pkg/flow/record_test.go
index 9564f970..31e18c22 100644
--- a/pkg/flow/record_test.go
+++ b/pkg/flow/record_test.go
@@ -1,6 +1,7 @@
 package flow
 
 import (
+	"fmt"
 	"net/http"
 	"testing"
 
@@ -359,13 +360,13 @@ func TestRecordState(t *testing.T) {
 
 func TestParameters(t *testing.T) {
 	type test struct {
-		values      map[string]string
+		url         string
 		queryParams QueryParams
 	}
 
 	testTable := []test{
 		{
-			values: map[string]string{"timeRangeStart": "1234", "timeRangeEnd": "0"},
+			url: "http://host?timeRangeStart=1234&timeRangeEnd=0",
 			queryParams: QueryParams{
 				Offset:             -1,
 				Limit:              -1,
@@ -377,7 +378,7 @@ func TestParameters(t *testing.T) {
 			},
 		},
 		{
-			values: map[string]string{"timeRangeStart": "0", "timeRangeEnd": "0", "timeRangeOperation": "contains"},
+			url: "http://host?timeRangeStart=0&timeRangeEnd=0&timeRangeOperation=contains",
 			queryParams: QueryParams{
 				Offset:             -1,
 				Limit:              -1,
@@ -389,7 +390,7 @@ func TestParameters(t *testing.T) {
 			},
 		},
 		{
-			values: map[string]string{"timeRangeStart": "1234", "timeRangeEnd": "0", "timeRangeOperation": "within"},
+			url: "http://host?timeRangeStart=1234&timeRangeEnd=0&timeRangeOperation=within",
 			queryParams: QueryParams{
 				Offset:             -1,
 				Limit:              -1,
@@ -401,15 +402,7 @@ func TestParameters(t *testing.T) {
 			},
 		},
 		{
-			values: map[string]string{
-				"offset":             "10",
-				"limit":              "10",
-				"sortBy":             "sourcePort.desc",
-				"filter":             "forwardFlow.protocol.tcp",
-				"timeRangeStart":     "0",
-				"timeRangeEnd":       "4567",
-				"timeRangeOperation": "intersects",
-			},
+			url: "http://host?timeRangeStart=0&timeRangeEnd=4567&timeRangeOperation=intersects&offset=10&limit=10&sortBy=sourcePort.desc&filter=forwardFlow.protocol.tcp",
 			queryParams: QueryParams{
 				Offset:             10,
 				Limit:              10,
@@ -420,22 +413,30 @@ func TestParameters(t *testing.T) {
 				TimeRangeOperation: intersects,
 			},
 		},
+		{
+			url: "http://host?processRole=external&processRole=internal&timeRangeStart=0&timeRangeEnd=0&limit=0&offset=0",
+			queryParams: QueryParams{
+				SortBy: "identity.asc",
+				FilterFields: map[string][]string{
+					"processRole": {"external", "internal"},
+				},
+			},
+		},
 	}
 
 	for _, test := range testTable {
-		req, _ := http.NewRequest("GET", "/", nil)
-		q := req.URL.Query()
-		for k, v := range test.values {
-			q.Add(k, v)
-		}
-		req.URL.RawQuery = q.Encode()
-		qp := getQueryParams(req.URL)
-		assert.Equal(t, qp.Offset, test.queryParams.Offset)
-		assert.Equal(t, qp.Limit, test.queryParams.Limit)
-		assert.Equal(t, qp.SortBy, test.queryParams.SortBy)
-		assert.Equal(t, qp.TimeRangeStart, test.queryParams.TimeRangeStart)
-		assert.Equal(t, qp.TimeRangeEnd, test.queryParams.TimeRangeEnd)
-		assert.Equal(t, qp.TimeRangeOperation, test.queryParams.TimeRangeOperation)
+		t.Run(test.url, func(t *testing.T) {
+			req, _ := http.NewRequest("GET", test.url, nil)
+			q := req.URL.Query()
+			req.URL.RawQuery = q.Encode()
+			qp := getQueryParams(req.URL)
+			assert.Equal(t, qp.Offset, test.queryParams.Offset)
+			assert.Equal(t, qp.Limit, test.queryParams.Limit)
+			assert.Equal(t, qp.SortBy, test.queryParams.SortBy)
+			assert.Equal(t, qp.TimeRangeStart, test.queryParams.TimeRangeStart)
+			assert.Equal(t, qp.TimeRangeEnd, test.queryParams.TimeRangeEnd)
+			assert.Equal(t, qp.TimeRangeOperation, test.queryParams.TimeRangeOperation)
+		})
 	}
 }
 
@@ -488,27 +489,27 @@ func TestPagination(t *testing.T) {
 
 func TestMatchField(t *testing.T) {
 	field1 := "foo"
-	field1Value := "foo"
+	field1Value := []string{"foo"}
 	field2 := uint64(12345678)
-	field2Value := "12345678"
+	field2Value := []string{"12345678"}
 	field3 := int32(87654321)
-	field3Value := "87654321"
+	field3Value := []string{"87654321"}
 	field4 := int64(12345678)
-	field4Value := "12345678"
+	field4Value := []string{"12345678"}
 	field5 := int(12345678)
-	field5Value := "12345678"
+	field5Value := []string{"12345678"}
 
-	match := matchFieldValue(field1, field1Value)
+	match := matchFieldValues(field1, field1Value)
 	assert.Equal(t, match, true)
-	match = matchFieldValue(field2, field2Value)
+	match = matchFieldValues(field2, field2Value)
 	assert.Equal(t, match, true)
-	match = matchFieldValue(field3, field3Value)
+	match = matchFieldValues(field3, field3Value)
 	assert.Equal(t, match, true)
-	match = matchFieldValue(field4, field4Value)
+	match = matchFieldValues(field4, field4Value)
 	assert.Equal(t, match, true)
-	match = matchFieldValue(field5, field5Value)
+	match = matchFieldValues(field5, field5Value)
 	assert.Equal(t, match, true)
-	match = matchFieldValue(field5, field1Value)
+	match = matchFieldValues(field5, field1Value)
 	assert.Equal(t, match, false)
 }
 
@@ -664,52 +665,66 @@ func TestFilterFieldsRecord(t *testing.T) {
 	}
 
 	type test struct {
-		filterField map[string]string
+		filterField map[string][]string
 		result      bool
 	}
 	testTable := []test{
 		{
-			filterField: map[string]string{"forwardFlow.SourceHost": "10.20.30.40"},
+			filterField: map[string][]string{"forwardFlow.SourceHost": {"10.20.30.40"}},
+			result:      true,
+		},
+		{
+			filterField: map[string][]string{"sourceSiteName": {"public1"}},
 			result:      true,
 		},
 		{
-			filterField: map[string]string{"sourceSiteName": "public1"},
+			filterField: map[string][]string{"sourceSiteName": {"pub"}},
 			result:      true,
 		},
 		{
-			filterField: map[string]string{"sourceSiteName": "pub"},
+			filterField: map[string][]string{"sourceSiteName": {"pub", "xyz"}},
 			result:      true,
 		},
 		{
-			filterField: map[string]string{"sourceSiteName": "ic1"},
+			filterField: map[string][]string{"sourceSiteName": {"ic1"}},
 			result:      false,
 		},
 		{
-			filterField: map[string]string{"sourceSiteName": "xpublic1"},
+			filterField: map[string][]string{"sourceSiteName": {"xpublic1"}},
 			result:      false,
 		},
 		{
-			filterField: map[string]string{"forwardFlow.SourceHost": "10.20.30.40", "sourceSiteName": "public1"},
+			filterField: map[string][]string{"sourceSiteName": {"xpublic1", "zpublic1"}},
+			result:      false,
+		},
+		{
+			filterField: map[string][]string{"forwardFlow.SourceHost": {"10.20.30.40"}, "sourceSiteName": {"public1"}},
 			result:      true,
 		},
 		{
-			filterField: map[string]string{"forwardFlow.SourceHost": "10.20.30.40", "sourceSiteName": "public2"},
+			filterField: map[string][]string{"forwardFlow.SourceHost": {"10.20.255.255", "10.20.30.40"}, "sourceSiteName": {"public1"}},
+			result:      true,
+		},
+		{
+			filterField: map[string][]string{"forwardFlow.SourceHost": {"10.20.30.40"}, "sourceSiteName": {"public2"}},
 			result:      false,
 		},
 		{
-			filterField: map[string]string{},
+			filterField: map[string][]string{},
 			result:      true,
 		},
 		{
-			filterField: map[string]string{"sourceSiteName": ""},
+			filterField: map[string][]string{"sourceSiteName": {""}},
 			result:      false,
 		},
 	}
 
-	for _, test := range testTable {
-		qp := QueryParams{FilterFields: test.filterField}
-		result := filterRecord(flowPair, qp)
-		assert.Equal(t, result, test.result)
+	for i, test := range testTable {
+		t.Run(fmt.Sprintf("test-%d", i), func(t *testing.T) {
+			qp := QueryParams{FilterFields: test.filterField}
+			result := filterRecord(flowPair, qp)
+			assert.Equal(t, result, test.result)
+		})
 	}
 }
 
diff --git a/test/integration/examples/custom/helloworld/helloworld_podman_test.go b/test/integration/examples/custom/helloworld/helloworld_podman_test.go
index 2615d7c6..10292883 100644
--- a/test/integration/examples/custom/helloworld/helloworld_podman_test.go
+++ b/test/integration/examples/custom/helloworld/helloworld_podman_test.go
@@ -4,6 +4,7 @@ package helloworld
 
 import (
 	"bytes"
+	"context"
 	"fmt"
 	"log"
 	"os"
@@ -702,7 +703,7 @@ func deployPodmanResources() error {
 		// podman network exists, but DNS is not enabled
 		return fmt.Errorf("podman network %s already exists, but DNS is not enabled", PodmanNetwork)
 	}
-	err = podmanCli.ImagePull("quay.io/skupper/hello-world-backend")
+	err = podmanCli.ImagePull(context.Background(), "quay.io/skupper/hello-world-backend")
 	if err != nil {
 		return err
 	}