Repository: skupperproject/skupper
Pull request #1357 (2024-01-16 19:35:21)

Proposal to use explicit resources for service bindings


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 5d97bc20..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)
 	}
diff --git a/client/podman/image.go b/client/podman/image.go
index 2be5f250..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"
@@ -71,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)
@@ -94,7 +95,7 @@ func (p *PodmanRestClient) ImagePull(id string) error {
 		Schemes:            []string{"http", "https"},
 		Params:             params,
 		Reader:             reader,
-		Context:            params.Context,
+		Context:            ctx,
 		Client:             params.HTTPClient,
 	}
 	res, err := p.RestClient.Submit(op)
diff --git a/client/router_update.go b/client/router_update.go
index 53437fb8..6ce0be06 100644
--- a/client/router_update.go
+++ b/client/router_update.go
@@ -103,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
@@ -116,7 +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")
+		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
 	}
@@ -141,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)
@@ -524,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_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/doc/adr/0006-explicit-resources-for-declaring-service-bindings.md b/doc/adr/0006-explicit-resources-for-declaring-service-bindings.md
new file mode 100644
index 00000000..85a5c334
--- /dev/null
+++ b/doc/adr/0006-explicit-resources-for-declaring-service-bindings.md
@@ -0,0 +1,73 @@
+# 6. Explicit resources for declaring service bindings
+
+Date: 2024-01-16
+
+## Status
+
+Proposed
+
+Enables [7. Deprecate ServiceSync](0007-deprecate-servicesync.md)
+
+## Context
+
+Service bindings are the configurations that determine how a Skupper
+service is invoked (ingress binding) and what processes that
+invocation may be routed to (egress binding).
+
+The Deployments or StatefulSets that are to be annotated may
+themselves be the generated output of other controllers. In this case
+it is not always possible to ensure that they are annotated correctly
+for Skupper.
+
+The way annotations work when applied to Service resources is
+confusing and unintuitive.
+
+If the skupper.io/proxy annotation is found on a Service, the egress
+binding is configured based on the original selector in the Service
+and this selector is then changed to point the service at the router
+pods. This changing of the original resource can cause problems when a
+GitOps approach is used for deployment.
+
+However if the skupper.io/target annotation is also applied to the
+Service, the behaviour is different. Here the annotated service is
+updated to point at the skupper router pods, and the value of target
+is used to configure the egress binding.
+
+If instead the skupper.io/ingress-only annotation is also present on
+the Service, no target is inferred. The service is configured to point
+at the router pods on a port that will route to the address specified
+by the skupper.io/address annotation.
+
+The code accummulated to handle all these configurations as well as
+the CLI is not the easiest to maintain.
+
+## Decision
+
+Explicit resources will be introduced that describe the desired
+configuration for both ingress- and egress- bindings.
+
+## Consequences
+
+Support for the existing annotations, such as skupper.io/proxy, will
+be removed. It may be separately decided to provide legacy support in
+some fashion that would derive the new explicit bindings from
+annotated resources.
+
+It is expected that a model made explicit in the resources used will
+be easier for users to understand and internalise, making it more
+likely that they are successful in their use of skupper and reducing
+their need to seek help with simple configuration.
+
+Explicit bindings resources will also give more flexibility, for
+example allowing more than one ingress binding for the same service if
+needed, or allowing the hostname used for the ingress to vary by site.
+
+However a change to the model for service bindings will make upgrade
+and backward compatibility more of a challenge.
+
+The change in model will need to be clearly documented and explained
+to users, along with the recommended path for migration to this new
+model.
+
+The change would also impact existing examples, both under the
+skupperproject organisation and elsewhere.
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)


Test patch:
diff --git a/client/podman/container_test.go b/client/podman/container_test.go
index 251c2586..e7829845 100644
--- a/client/podman/container_test.go
+++ b/client/podman/container_test.go
@@ -115,12 +115,19 @@ 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(invalidImage)
+		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
 	t.Run("container-create", func(t *testing.T) {
@@ -205,7 +212,7 @@ 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)
@@ -214,7 +221,7 @@ func TestContainer(t *testing.T) {
 
 	t.Run("container-logs", func(t *testing.T) {
 		clogsName := RandomName("skupper-test")
-		assert.Assert(t, cli.ImagePull(images.GetRouterImageName()))
+		assert.Assert(t, cli.ImagePull(ctx, images.GetRouterImageName()))
 		err = cli.ContainerCreate(&container.Container{
 			Name:        clogsName,
 			Image:       images.GetRouterImageName(),
@@ -295,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
 	}